using System.Collections.Generic; using System.Diagnostics; using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Reflection; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Invocation; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; namespace Robust.Shared.Toolshed; /// /// The overarching controller for Toolshed, providing invocation, reflection, commands, parsing, and other tools used by the language. /// External documentation has a more in-depth look. /// /// /// public sealed partial class ToolshedManager { [Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!; [Dependency] private readonly IEntityManager _entity = default!; [Dependency] private readonly IReflectionManager _reflection = default!; [Dependency] private readonly ILogManager _logManager = default!; #if !CLIENT_SCRIPTING [Dependency] private readonly INetManager _net = default!; #endif [Dependency] private readonly ISharedPlayerManager _player = default!; [Dependency] private readonly IConsoleHost _conHost = default!; private ISawmill _log = default!; private Dictionary _contexts = new(); /// /// If you're not an engine developer, you probably shouldn't call this. /// public void Initialize() { _log = _logManager.GetSawmill("toolshed"); InitializeParser(); _player.PlayerStatusChanged += OnStatusChanged; } private void OnStatusChanged(object? sender, SessionStatusEventArgs e) { if (!_contexts.TryGetValue(e.Session.UserId, out var ctx)) return; DebugTools.Assert(ctx.User == e.Session.UserId); if (e.NewStatus == SessionStatus.Disconnected) { DebugTools.Assert(ctx.Session == e.Session); ctx.Shell = null; } if (e.NewStatus == SessionStatus.InGame) { DebugTools.AssertNull(ctx.Session); DebugTools.AssertNull(ctx.Shell); ctx.Shell = new ConsoleShell(_conHost, e.Session, false); } } /// /// Invokes a command as the given user. /// /// User to run as. /// Command to invoke. /// An input value to use, if any. /// The resulting value, if any. /// Invocation success. /// /// ToolshedManager toolshed = ...; /// ICommonSession ctx = ...; /// // Now run some user provided command and get a result! /// toolshed.InvokeCommand(ctx, userCommand, "my input value", out var result); /// /// /// This will use the same IInvocationContext as the one used by the user for debug console commands. /// public bool InvokeCommand(ICommonSession session, string command, object? input, out object? result) { if (!_contexts.TryGetValue(session.UserId, out var ctx)) { // Can't get a shell here. result = null; return false; } ctx.ClearErrors(); return InvokeCommand(ctx, command, input, out result); } /// /// Invokes a command as the given user. /// /// User to run as. /// Command to invoke. /// An input value to use, if any. /// The resulting value, if any. /// Invocation success. /// /// ToolshedManager toolshed = ...; /// IConsoleShell ctx = ...; /// // Now run some user provided command and get a result! /// toolshed.InvokeCommand(ctx, userCommand, "my input value", out var result); /// /// /// This will use the same IInvocationContext as the one used by the user for debug console commands. /// public bool InvokeCommand(IConsoleShell session, string command, object? input, out object? result, out IInvocationContext ctx) { var idx = session.Player?.UserId ?? new NetUserId(); if (!_contexts.TryGetValue(idx, out var ourCtx)) { ourCtx = new OldShellInvocationContext(session); _contexts[idx] = ourCtx; } ourCtx.ClearErrors(); ctx = ourCtx; return InvokeCommand(ctx, command, input, out result); } /// /// Invokes a command with the given context. /// /// The context to run in. /// Command to invoke. /// An input value to use, if any. /// The resulting value, if any. /// Invocation success. /// /// ToolshedManager toolshed = ...; /// IInvocationContext ctx = ...; /// // Now run some user provided command and get a result! /// toolshed.InvokeCommand(ctx, userCommand, "my input value", out var result); /// public bool InvokeCommand(IInvocationContext ctx, string command, object? input, out object? result) { ctx.ClearErrors(); result = null; var parser = new ParserContext(command, this, ctx); if (!CommandRun.TryParse(parser, input?.GetType(), null, out var expr)) { ctx.ReportError(parser.Error ?? new FailedToParseError()); return false; } result = expr.Invoke(input, ctx); return true; } public CompletionResult? GetCompletions(ConsoleShell shell, string command) { var idx = shell.Player?.UserId ?? new NetUserId(); if (!_contexts.TryGetValue(idx, out var ourCtx)) ourCtx = _contexts[idx] = new OldShellInvocationContext(shell); return GetCompletions(ourCtx, command); } public CompletionResult? GetCompletions(IInvocationContext ctx, string command) { ctx.ClearErrors(); var parser = new ParserContext(command, this, ctx); parser.GenerateCompletions = true; CommandRun.TryParse(parser, null, null, out _); return parser.Completions; } } /// /// A command specification, containing both the command object and the selected subcommand if any. /// /// Command object. /// Subcommand, if any. public readonly record struct CommandSpec(ToolshedCommand Cmd, string? SubCommand) : IAsType { /// public ToolshedCommand AsType() { return Cmd; } /// /// Returns a completion option for this command, for use in autocomplete. /// public CompletionOption AsCompletion() { return new CompletionOption(FullName(), Cmd.Description(SubCommand)); } /// /// Returns the full name of the command. /// public string FullName() => SubCommand == null ? Cmd.Name : $"{Cmd.Name}:{SubCommand}"; /// /// Returns the localization string for the description of this command. /// public string DescLocStr() => Cmd.DescriptionLocKey(SubCommand); /// public override string ToString() => FullName(); } public record struct FailedToParseError() : IConError { public FormattedMessage DescribeInner() { return FormattedMessage.FromUnformatted($"Failed to parse toolshed command"); } public string? Expression { get; set; } public Vector2i? IssueSpan { get; set; } public StackTrace? Trace { get; set; } }