using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Robust.Shared.Enums; using Robust.Shared.IoC; using Robust.Shared.IoC.Exceptions; using Robust.Shared.Localization; using Robust.Shared.Log; using Robust.Shared.Network; using Robust.Shared.Players; using Robust.Shared.Reflection; using Robust.Shared.Timing; using Robust.Shared.Utility; using Robust.Shared.ViewVariables; namespace Robust.Shared.Console { /// public abstract class ConsoleHost : IConsoleHost { protected const string SawmillName = "con"; [Dependency] protected readonly ILogManager LogManager = 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!; [Dependency] protected readonly ILocalizationManager LocalizationManager = default!; [ViewVariables] protected readonly Dictionary RegisteredCommands = new(); private readonly CommandBuffer _commandBuffer = new CommandBuffer(); /// public bool IsServer { get; } /// public IConsoleShell LocalShell { get; } /// public virtual IReadOnlyDictionary AvailableCommands => RegisteredCommands; public abstract event ConAnyCommandCallback? AnyCommandExecuted; protected ConsoleHost(bool isServer) { IsServer = isServer; LocalShell = new ConsoleShell(this, null, true); } /// public event EventHandler? ClearText; /// public void LoadConsoleCommands() { // search for all client commands in all assemblies, and register them foreach (var type in ReflectionManager.GetAllChildren()) { var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true); if (AvailableCommands.TryGetValue(instance.Command, out var duplicate)) { throw new InvalidImplementationException(instance.GetType(), typeof(IConsoleCommand), $"Command name already registered: {instance.Command}, previous: {duplicate.GetType()}"); } RegisteredCommands[instance.Command] = instance; } } protected virtual void UpdateAvailableCommands() { } #region RegisterCommand public void RegisterCommand( string command, string description, string help, ConCommandCallback callback, bool requireServerOrSingleplayer = false) { if (RegisteredCommands.ContainsKey(command)) throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, requireServerOrSingleplayer); RegisteredCommands.Add(command, newCmd); UpdateAvailableCommands(); } public void RegisterCommand( string command, string description, string help, ConCommandCallback callback, ConCommandCompletionCallback completionCallback, bool requireServerOrSingleplayer = false) { if (RegisteredCommands.ContainsKey(command)) throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); RegisteredCommands.Add(command, newCmd); UpdateAvailableCommands(); } public void RegisterCommand( string command, string description, string help, ConCommandCallback callback, ConCommandCompletionAsyncCallback completionCallback, bool requireServerOrSingleplayer = false) { if (RegisteredCommands.ContainsKey(command)) throw new InvalidOperationException($"Command already registered: {command}"); var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); RegisteredCommands.Add(command, newCmd); UpdateAvailableCommands(); } public void RegisterCommand(string command, ConCommandCallback callback, bool requireServerOrSingleplayer = false) { var description = LocalizationManager.TryGetString($"cmd-{command}-desc", out var desc) ? desc : ""; var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : ""; RegisterCommand(command, description, help, callback, requireServerOrSingleplayer); } public void RegisterCommand( string command, ConCommandCallback callback, ConCommandCompletionCallback completionCallback, bool requireServerOrSingleplayer = false) { var description = LocalizationManager.TryGetString($"cmd-{command}-desc", out var desc) ? desc : ""; var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : ""; RegisterCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); } public void RegisterCommand( string command, ConCommandCallback callback, ConCommandCompletionAsyncCallback completionCallback, bool requireServerOrSingleplayer = false) { var description = LocalizationManager.TryGetString($"cmd-{command}-desc", out var desc) ? desc : ""; var help = LocalizationManager.TryGetString($"cmd-{command}-help", out var val) ? val : ""; RegisterCommand(command, description, help, callback, completionCallback, requireServerOrSingleplayer); } #endregion /// public void UnregisterCommand(string command) { if (!RegisteredCommands.TryGetValue(command, out var cmd)) throw new KeyNotFoundException($"Command {command} is not registered."); if (cmd is not RegisteredCommand) throw new InvalidOperationException( "You cannot unregister commands that have been registered automatically."); RegisteredCommands.Remove(command); UpdateAvailableCommands(); } //TODO: Pull up public abstract void ExecuteCommand(ICommonSession? session, string command); //TODO: server -> client forwarding, making the system asymmetrical public abstract void RemoteExecuteCommand(ICommonSession? session, string command); //TODO: IConsoleOutput for [e#1225] public abstract void WriteLine(ICommonSession? session, string text); public abstract void WriteLine(ICommonSession? session, FormattedMessage msg); public abstract void WriteError(ICommonSession? session, string text); /// public void ClearLocalConsole() { ClearText?.Invoke(this, EventArgs.Empty); } /// public IConsoleShell GetSessionShell(ICommonSession session) { if (!IsServer) return LocalShell; if (session.Status >= SessionStatus.Disconnected) throw new InvalidOperationException("Tried to get the session shell of a disconnected peer."); return new ConsoleShell(this, session, false); } /// public void ExecuteCommand(string command) { 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 . /// [Reflect(false)] public sealed class RegisteredCommand : IConsoleCommand { public ConCommandCallback Callback { get; } public ConCommandCompletionCallback? CompletionCallback { get; } public ConCommandCompletionAsyncCallback? CompletionCallbackAsync { get; } /// public string Command { get; } /// public string Description { get; } /// public string Help { get; } /// public bool RequireServerOrSingleplayer { get; init; } /// /// Constructs a new instance of . /// /// Name of the command. /// Short description of the command. /// Extended description for the command. /// Callback function that is ran when the command is executed. /// Callback function to get console completions. public RegisteredCommand( string command, string description, string help, ConCommandCallback callback, bool requireServerOrSingleplayer = false) { Command = command; // Should these two be localized somehow? Description = description; Help = help; Callback = callback; RequireServerOrSingleplayer = requireServerOrSingleplayer; } /// /// Constructs a new instance of . /// /// Name of the command. /// Short description of the command. /// Extended description for the command. /// Callback function that is ran when the command is executed. /// Callback function to get console completions. public RegisteredCommand( string command, string description, string help, ConCommandCallback callback, ConCommandCompletionCallback completionCallback, bool requireServerOrSingleplayer = false) : this(command, description, help, callback, requireServerOrSingleplayer) { CompletionCallback = completionCallback; } /// /// Constructs a new instance of . /// /// Name of the command. /// Short description of the command. /// Extended description for the command. /// Callback function that is ran when the command is executed. /// Asynchronous callback function to get console completions. public RegisteredCommand( string command, string description, string help, ConCommandCallback callback, ConCommandCompletionAsyncCallback completionCallback, bool requireServerOrSingleplayer = false) : this(command, description, help, callback, requireServerOrSingleplayer) { CompletionCallbackAsync = completionCallback; } /// public void Execute(IConsoleShell shell, string argStr, string[] args) { Callback(shell, argStr, args); } public ValueTask GetCompletionAsync( IConsoleShell shell, string[] args, string argStr, CancellationToken cancel) { if (CompletionCallbackAsync != null) return CompletionCallbackAsync(shell, args, argStr); if (CompletionCallback != null) return ValueTask.FromResult(CompletionCallback(shell, args)); return ValueTask.FromResult(CompletionResult.Empty); } } } }