diff --git a/Robust.Client/Console/ClientConsoleHost.cs b/Robust.Client/Console/ClientConsoleHost.cs index dcc5d4579..a2a43403c 100644 --- a/Robust.Client/Console/ClientConsoleHost.cs +++ b/Robust.Client/Console/ClientConsoleHost.cs @@ -109,13 +109,13 @@ namespace Robust.Client.Console if (AvailableCommands.ContainsKey(commandName)) { var playerManager = IoCManager.Resolve(); - +#if !DEBUG if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting) { WriteError(null, $"Insufficient perms for command: {commandName}"); return; } - +#endif var command1 = AvailableCommands[commandName]; args.RemoveAt(0); var shell = new ConsoleShell(this, null); diff --git a/Robust.Client/GameController/GameController.cs b/Robust.Client/GameController/GameController.cs index 56ca357e5..11c461929 100644 --- a/Robust.Client/GameController/GameController.cs +++ b/Robust.Client/GameController/GameController.cs @@ -455,6 +455,7 @@ namespace Robust.Client private void Tick(FrameEventArgs frameEventArgs) { _modLoader.BroadcastUpdate(ModUpdateLevel.PreEngine, frameEventArgs); + _console.CommandBufferExecute(); _timerManager.UpdateTimers(frameEventArgs); _taskManager.ProcessPendingTasks(); diff --git a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs index a7e250436..9a5623b57 100644 --- a/Robust.Client/GameObjects/EntitySystems/InputSystem.cs +++ b/Robust.Client/GameObjects/EntitySystems/InputSystem.cs @@ -4,11 +4,15 @@ using Robust.Client.Input; using Robust.Client.Player; using Robust.Shared; using Robust.Shared.Configuration; +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.Maths; using Robust.Shared.Players; +using Robust.Shared.Timing; using Robust.Shared.Utility; namespace Robust.Client.GameObjects @@ -21,6 +25,8 @@ namespace Robust.Client.GameObjects [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!; private readonly IPlayerCommandStates _cmdStates = new PlayerCommandStates(); @@ -108,6 +114,43 @@ namespace Robust.Client.GameObjects 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) + { + var localPlayer = _playerManager.LocalPlayer; + if(localPlayer is null) + return; + + var pent = localPlayer.ControlledEntity; + if(pent is null) + return; + + BoundKeyFunction keyFunction = new BoundKeyFunction(args[0]); + BoundKeyState state = args[1] == "u" ? BoundKeyState.Up: BoundKeyState.Down; + + var pxform = Transform(pent.Value); + var wPos = pxform.WorldPosition + new Vector2(float.Parse(args[2]), float.Parse(args[3])); + var coords = EntityCoordinates.FromMap(EntityManager, pent.Value, new MapCoordinates(wPos, pxform.MapID)); + + var funcId = _inputManager.NetworkBindMap.KeyFunctionID(keyFunction); + + var message = new FullInputCmdMessage(_timing.CurTick, _timing.TickFraction, funcId, state, + coords, new ScreenCoordinates(0, 0, default), EntityUid.Invalid); + + HandleInputCommand(localPlayer.Session, keyFunction, message); } private void OnAttachedEntityChanged(PlayerAttachSysMessage message) diff --git a/Robust.Server/BaseServer.cs b/Robust.Server/BaseServer.cs index 662a89196..6819a7197 100644 --- a/Robust.Server/BaseServer.cs +++ b/Robust.Server/BaseServer.cs @@ -659,6 +659,7 @@ namespace Robust.Server using (TickUsage.WithLabels("Timers").NewTimer()) { + _consoleHost.CommandBufferExecute(); timerManager.UpdateTimers(frameEventArgs); } diff --git a/Robust.Shared/Console/CommandBuffer.cs b/Robust.Shared/Console/CommandBuffer.cs new file mode 100644 index 000000000..5edd75d8c --- /dev/null +++ b/Robust.Shared/Console/CommandBuffer.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Robust.Shared.Console; + +public class CommandBuffer +{ + private const string DelayMarker = "-DELAY-"; + + private int _tickrate = 0; + private int _delay = 0; + + private readonly LinkedList _commandBuffer = new(); + + public void Append(string command) + { + _commandBuffer.AddLast(command); + } + + public void Insert(string command) + { + _commandBuffer.AddFirst(command); + } + + public void Tick(byte tickRate) + { + _tickrate = tickRate; + + if (_delay > 0) + { + _delay -= 1; + } + } + + public bool TryGetCommand([MaybeNullWhen(false)]out string command) + { + var next = _commandBuffer.First; + + if (next is null) // nothing to do here + { + command = null; + return false; + } + + if (next.Value.Equals(DelayMarker)) + { + if (_delay == 0) // just finished + { + _commandBuffer.RemoveFirst(); + return TryGetCommand(out command); + } + else // currently counting down delay + { + command = null; + return false; + } + } + + if (next.Value.StartsWith("wait ")) + { + var sTicks = next.Value.Substring(5); + _commandBuffer.RemoveFirst(); + if (string.IsNullOrWhiteSpace(sTicks) || !int.TryParse(sTicks, out var ticks)) // messed up command + { + return TryGetCommand(out command); + } + + // Setup Timing + _commandBuffer.AddFirst(DelayMarker); + _delay = ticks; + + command = null; + return false; + } + + // normal command + _commandBuffer.RemoveFirst(); + command = next.Value; + return true; + } +} diff --git a/Robust.Shared/Console/Commands/ExecCommand.cs b/Robust.Shared/Console/Commands/ExecCommand.cs index 1ac1a56b4..e53960e46 100644 --- a/Robust.Shared/Console/Commands/ExecCommand.cs +++ b/Robust.Shared/Console/Commands/ExecCommand.cs @@ -48,7 +48,7 @@ namespace Robust.Shared.Console.Commands continue; } - shell.ExecuteCommand(line); + shell.ConsoleHost.AppendCommand(line); } } } diff --git a/Robust.Shared/Console/ConsoleHost.cs b/Robust.Shared/Console/ConsoleHost.cs index a62d29f89..a2f66ceec 100644 --- a/Robust.Shared/Console/ConsoleHost.cs +++ b/Robust.Shared/Console/ConsoleHost.cs @@ -7,6 +7,7 @@ using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Reflection; +using Robust.Shared.Timing; using Robust.Shared.ViewVariables; namespace Robust.Shared.Console @@ -17,13 +18,16 @@ namespace Robust.Shared.Console protected const string SawmillName = "con"; [Dependency] protected readonly ILogManager LogManager = default!; - [Dependency] protected readonly IReflectionManager ReflectionManager = default!; + [Dependency] private readonly IReflectionManager ReflectionManager = default!; [Dependency] protected readonly INetManager NetManager = default!; [Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!; + [Dependency] private readonly IGameTiming _timing = default!; [ViewVariables] protected readonly Dictionary AvailableCommands = new(); + private readonly CommandBuffer _commandBuffer = new CommandBuffer(); + /// public bool IsServer => NetManager.IsServer; @@ -116,6 +120,36 @@ namespace Robust.Shared.Console ExecuteCommand(null, command); } + /// + public void AppendCommand(string command) + { + _commandBuffer.Append(command); + } + + /// + public void InsertCommand(string command) + { + _commandBuffer.Insert(command); + } + + /// + public void CommandBufferExecute() + { + _commandBuffer.Tick(_timing.TickRate); + + while (_commandBuffer.TryGetCommand(out var cmd)) + { + try + { + ExecuteCommand(cmd); + } + catch (Exception e) + { + LocalShell.WriteError(e.Message); + } + } + } + /// /// A console command that was registered inline through . /// diff --git a/Robust.Shared/Console/IConsoleHost.cs b/Robust.Shared/Console/IConsoleHost.cs index da86805df..b5a969e92 100644 --- a/Robust.Shared/Console/IConsoleHost.cs +++ b/Robust.Shared/Console/IConsoleHost.cs @@ -81,11 +81,35 @@ namespace Robust.Shared.Console IConsoleShell GetSessionShell(ICommonSession session); /// - /// Execute a command string on the local shell. + /// Execute a command string immediately on the local shell, bypassing the command buffer completely. /// /// Command string to execute. void ExecuteCommand(string command); + /// + /// Appends a command into the end of the command buffer on the local shell. + /// + /// + /// This command will be ran *sometime* in the future, depending on how many waits are in the buffer. + /// + /// Command string to execute. + void AppendCommand(string command); + + /// + /// Inserts a command into the front of the command buffer on the local shell. + /// + /// + /// This command will preempt the next command executed in the command buffer. + /// + /// Command string to execute. + void InsertCommand(string command); + + /// + /// Processes any contents of the command buffer on the local shell. This needs to be called regularly (once a tick), + /// inside the simulation. Pausing the server should prevent the buffer from being processed. + /// + void CommandBufferExecute(); + /// /// Executes a command string on this specific session shell. If the command does not exist, the command will be forwarded /// to the