mirror of
https://github.com/space-wizards/RobustToolbox.git
synced 2026-02-15 03:30:53 +01:00
Console command completions v1. (#2817)
* Console command completions v1.
I think it works™️
* Unify cvar commands
* Handle no-completions-at-all better.
* Don't crash if you tab complete while no completions available.
* Always show hints if available
* Properly null completion hint over the wire
* Unify help command, localize it.
* Clean up + localize cvar command.
* Remove debug logging
* List command unified & localized.
* Remove server completions debug logging
* Remote execute command.
Had to make everything async for this.
* Don't lower case enums or bools
Why
* GC commands converted and localized.
* Fix remote command completions.
Whoops
* Kick command completions
* lsasm unified & localized.
* Revert "Don't lower case enums or bools"
This reverts commit 2f825347c3.
* ToString gc_mode command enums instead of trying to fix Fluent.
Ah well.
* Unify szr_stats
* Unify log commands, completions
* Fix compile
* Improve completion with complex cases (quotes, escapes)
* Code cleanup, comments.
* Fix tab completion with empty arg ruining everything.
* Fix RegisteredCommand completions
* Add more complex completion options system.
* Refactor content directory entries into a proper resource manager API.
* Implement GetEntries for DirLoader
* Make type hint darker.
* Exec command autocomplete, pulled play global sound code out to engine.
This commit is contained in:
committed by
GitHub
parent
84a80db21f
commit
5057c91dcd
86
Resources/Locale/en-US/commands.ftl
Normal file
86
Resources/Locale/en-US/commands.ftl
Normal file
@@ -0,0 +1,86 @@
|
||||
### Localization for engine console commands
|
||||
|
||||
## 'help' command
|
||||
cmd-help-desc = Display general help or help text for a specific command
|
||||
cmd-help-help = Usage: help [command name]
|
||||
When no command name is provided, displays general-purpose help text. If a command name is provided, displays help text for that command.
|
||||
|
||||
cmd-help-no-args = To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'. To search for commands, use 'list <filter>'.
|
||||
cmd-help-unknown = Unknown command: { $command }
|
||||
cmd-help-top = { $command } - { $description }
|
||||
cmd-help-invalid-args = Invalid amount of arguments.
|
||||
cmd-help-arg-cmdname = [command name]
|
||||
|
||||
## 'cvar' command
|
||||
cmd-cvar-desc = Gets or sets a CVar.
|
||||
cmd-cvar-help = Usage: cvar <name | ?> [value]
|
||||
If a value is passed, the value is parsed and stored as the new value of the CVar.
|
||||
If not, the current value of the CVar is displayed.
|
||||
Use 'cvar ?' to get a list of all registered CVars.
|
||||
|
||||
cmd-cvar-invalid-args = Must provide exactly one or two arguments.
|
||||
cmd-cvar-not-registered = CVar '{ $cvar }' is not registered. Use 'cvar ?' to get a list of all registered CVars.
|
||||
cmd-cvar-parse-error = Input value is in incorrect format for type { $type }
|
||||
cmd-cvar-compl-list = List available CVars
|
||||
cmd-cvar-arg-name = <name | ?>
|
||||
|
||||
## 'list' command
|
||||
cmd-list-desc = Lists available commands, with optional search filter
|
||||
cmd-list-help = Usage: list [filter]
|
||||
Lists all available commands. If an argument is provided, it will be used to filter commands by name.
|
||||
|
||||
cmd-list-heading = SIDE NAME DESC{"\u000A"}-------------------------{"\u000A"}
|
||||
|
||||
cmd-list-arg-filter = [filter]
|
||||
|
||||
## '>' command, aka remote exec
|
||||
cmd-remoteexec-desc = Executes server-side commands
|
||||
cmd-remoteexec-help = Usage: > <command> [arg] [arg] [arg...]
|
||||
Executes a command on the server. This is necessary if a command with the same name exists on the client, as simply running the command would run the client command first.
|
||||
|
||||
## 'gc' command
|
||||
cmd-gc-desc = Run the GC (Garbage Collector)
|
||||
cmd-gc-help = Usage: gc [generation]
|
||||
Uses GC.Collect() to execute the Garbage Collector.
|
||||
If an argument is provided, it is parsed as a GC generation number and GC.Collect(int) is used.
|
||||
Use the 'gfc' command to do an LOH-compacting full GC.
|
||||
cmd-gc-failed-parse = Failed to parse argument.
|
||||
cmd-gc-arg-generation = [generation]
|
||||
|
||||
## 'gcf' command
|
||||
cmd-gcf-desc = Run the GC, fully, compacting LOH and everything.
|
||||
cmd-gcf-help = Usage: gcf
|
||||
Does a full GC.Collect(2, GCCollectionMode.Forced, true, true) while also compacting LOH.
|
||||
This will probably lock up for hundreds of milliseconds, be warned.
|
||||
|
||||
## 'gc_mode' command
|
||||
cmd-gc_mode-desc = Change/Read the GC Latency mode
|
||||
cmd-gc_mode-help = Usage: gc_mode [type]
|
||||
If no argument is provided, returns the current GC latency mode.
|
||||
If an argument is passed, it is parsed as GCLatencyMode and set as the GC latency mode.
|
||||
|
||||
cmd-gc_mode-current = current gc latency mode: { $prevMode }
|
||||
cmd-gc_mode-possible = possible modes:
|
||||
cmd-gc_mode-option = - { $mode }
|
||||
cmd-gc_mode-unknown = unknown gc latency mode: { $arg }
|
||||
cmd-gc_mode-attempt = attempting gc latency mode change: { $prevMode } -> { $mode }
|
||||
cmd-gc_mode-result = resulting gc latency mode: { $mode }
|
||||
cmd-gc_mode-arg-type = [type]
|
||||
|
||||
## 'mem' command
|
||||
cmd-mem-desc = Prints managed memory info
|
||||
cmd-mem-help = Usage: mem
|
||||
|
||||
cmd-mem-report = Heap Size: { TOSTRING($heapSize, "N0") }
|
||||
Total Allocated: { TOSTRING($totalAllocated, "N0") }
|
||||
|
||||
## 'lsasm' command
|
||||
cmd-lsasm-desc = Lists loaded assemblies by load context
|
||||
cmd-lsasm-help = Usage: lsasm
|
||||
|
||||
## 'exec' command
|
||||
cmd-exec-desc = Executes a script file from the game's writeable user data
|
||||
cmd-exec-help = Usage: exec <fileName>
|
||||
Each line in the file is executed as a single command, unless it starts with a #
|
||||
|
||||
cmd-exec-arg-filename = <fileName>
|
||||
94
Robust.Client/Console/ClientConsoleHost.Completions.cs
Normal file
94
Robust.Client/Console/ClientConsoleHost.Completions.cs
Normal file
@@ -0,0 +1,94 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Network.Messages;
|
||||
|
||||
namespace Robust.Client.Console;
|
||||
|
||||
internal sealed partial class ClientConsoleHost
|
||||
{
|
||||
private readonly Dictionary<int, PendingCompletion> _completionsPending = new();
|
||||
private int _completionSeq;
|
||||
|
||||
|
||||
public async Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
// Last element is the command currently being typed. May be empty.
|
||||
|
||||
// Logger.Debug($"Running completions: {string.Join(", ", args)}");
|
||||
|
||||
var delay = _cfg.GetCVar(CVars.ConCompletionDelay);
|
||||
if (delay > 0)
|
||||
await Task.Delay((int)(delay * 1000), cancel);
|
||||
|
||||
return await CalcCompletions(args, cancel);
|
||||
}
|
||||
|
||||
private Task<CompletionResult> CalcCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
if (args.Count == 1)
|
||||
{
|
||||
// Typing out command name, handle this ourselves.
|
||||
var cmdOptions = CompletionResult.FromOptions(
|
||||
RegisteredCommands.Values.Select(c => new CompletionOption(c.Command, c.Description)));
|
||||
return Task.FromResult(cmdOptions);
|
||||
}
|
||||
|
||||
if (!RegisteredCommands.TryGetValue(args[0], out var cmd))
|
||||
return Task.FromResult(CompletionResult.Empty);
|
||||
|
||||
return cmd.GetCompletionAsync(LocalShell, args.ToArray()[1..], cancel).AsTask();
|
||||
}
|
||||
|
||||
private Task<CompletionResult> DoServerCompletions(List<string> args, CancellationToken cancel)
|
||||
{
|
||||
var tcs = new TaskCompletionSource<CompletionResult>();
|
||||
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
|
||||
var seq = _completionSeq++;
|
||||
|
||||
var pending = new PendingCompletion
|
||||
{
|
||||
Cts = cts,
|
||||
Tcs = tcs
|
||||
};
|
||||
|
||||
var msg = new MsgConCompletion
|
||||
{
|
||||
Args = args.ToArray(),
|
||||
Seq = seq
|
||||
};
|
||||
|
||||
cts.Token.Register(() =>
|
||||
{
|
||||
tcs.SetCanceled(cts.Token);
|
||||
cts.Dispose();
|
||||
_completionsPending.Remove(seq);
|
||||
}, true);
|
||||
|
||||
NetManager.ClientSendMessage(msg);
|
||||
|
||||
_completionsPending.Add(seq, pending);
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
private void ProcessCompletionResp(MsgConCompletionResp message)
|
||||
{
|
||||
if (!_completionsPending.TryGetValue(message.Seq, out var pending))
|
||||
return;
|
||||
|
||||
pending.Cts.Dispose();
|
||||
pending.Tcs.SetResult(message.Result);
|
||||
|
||||
_completionsPending.Remove(message.Seq);
|
||||
}
|
||||
|
||||
private struct PendingCompletion
|
||||
{
|
||||
public TaskCompletionSource<CompletionResult> Tcs;
|
||||
public CancellationTokenSource Cts;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.Log;
|
||||
using Robust.Client.Player;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Enums;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
@@ -41,9 +46,10 @@ namespace Robust.Client.Console
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="IClientConsoleHost" />
|
||||
internal sealed class ClientConsoleHost : ConsoleHost, IClientConsoleHost
|
||||
internal sealed partial class ClientConsoleHost : ConsoleHost, IClientConsoleHost, IConsoleHostInternal
|
||||
{
|
||||
[Dependency] private readonly IClientConGroupController _conGroup = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private bool _requestedCommands;
|
||||
|
||||
@@ -53,6 +59,8 @@ namespace Robust.Client.Console
|
||||
NetManager.RegisterNetMessage<MsgConCmdReg>(HandleConCmdReg);
|
||||
NetManager.RegisterNetMessage<MsgConCmdAck>(HandleConCmdAck);
|
||||
NetManager.RegisterNetMessage<MsgConCmd>(ProcessCommand);
|
||||
NetManager.RegisterNetMessage<MsgConCompletion>();
|
||||
NetManager.RegisterNetMessage<MsgConCompletionResp>(ProcessCompletionResp);
|
||||
|
||||
_requestedCommands = false;
|
||||
NetManager.Connected += OnNetworkConnected;
|
||||
@@ -88,6 +96,11 @@ namespace Robust.Client.Console
|
||||
OutputText(text, true, true);
|
||||
}
|
||||
|
||||
public bool IsCmdServer(IConsoleCommand cmd)
|
||||
{
|
||||
return cmd is ServerDummyCommand;
|
||||
}
|
||||
|
||||
public override event ConAnyCommandCallback? AnyCommandExecuted;
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -108,8 +121,8 @@ namespace Robust.Client.Console
|
||||
|
||||
if (AvailableCommands.ContainsKey(commandName))
|
||||
{
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
#if !DEBUG
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
if (!_conGroup.CanCommand(commandName) && playerManager.LocalPlayer?.Session.Status > SessionStatus.Connecting)
|
||||
{
|
||||
WriteError(null, $"Insufficient perms for command: {commandName}");
|
||||
@@ -203,31 +216,64 @@ namespace Robust.Client.Console
|
||||
|
||||
_requestedCommands = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// These dummies are made purely so list and help can list server-side commands.
|
||||
/// </summary>
|
||||
[Reflect(false)]
|
||||
internal sealed class ServerDummyCommand : IConsoleCommand
|
||||
{
|
||||
internal ServerDummyCommand(string command, string help, string description)
|
||||
/// <summary>
|
||||
/// These dummies are made purely so list and help can list server-side commands.
|
||||
/// </summary>
|
||||
[Reflect(false)]
|
||||
private sealed class ServerDummyCommand : IConsoleCommand
|
||||
{
|
||||
Command = command;
|
||||
Help = help;
|
||||
Description = description;
|
||||
internal ServerDummyCommand(string command, string help, string description)
|
||||
{
|
||||
Command = command;
|
||||
Help = help;
|
||||
Description = description;
|
||||
}
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Help { get; }
|
||||
|
||||
// Always forward to server.
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr);
|
||||
}
|
||||
|
||||
public async ValueTask<CompletionResult> GetCompletionAsync(
|
||||
IConsoleShell shell,
|
||||
string[] args,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var host = (ClientConsoleHost)shell.ConsoleHost;
|
||||
var argsList = args.ToList();
|
||||
argsList.Insert(0, Command);
|
||||
|
||||
return await host.DoServerCompletions(argsList, cancel);
|
||||
}
|
||||
}
|
||||
|
||||
public string Command { get; }
|
||||
|
||||
public string Description { get; }
|
||||
|
||||
public string Help { get; }
|
||||
|
||||
// Always forward to server.
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
private sealed class RemoteExecCommand : IConsoleCommand
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr);
|
||||
public string Command => ">";
|
||||
public string Description => Loc.GetString("cmd-remoteexec-desc");
|
||||
public string Help => Loc.GetString("cmd-remoteexec-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
shell.RemoteExecuteCommand(argStr["> ".Length..]);
|
||||
}
|
||||
|
||||
public async ValueTask<CompletionResult> GetCompletionAsync(
|
||||
IConsoleShell shell,
|
||||
string[] args,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
var host = (ClientConsoleHost)shell.ConsoleHost;
|
||||
return await host.DoServerCompletions(args.ToList(), cancel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
@@ -7,57 +5,6 @@ using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class CVarCommand : SharedCVarCommand, IConsoleCommand
|
||||
{
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 1 || args.Length > 2)
|
||||
{
|
||||
shell.WriteError("Must provide exactly one or two arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
var name = args[0];
|
||||
|
||||
if (name == "?")
|
||||
{
|
||||
var cvars = configManager.GetRegisteredCVars().OrderBy(c => c);
|
||||
shell.WriteLine(string.Join("\n", cvars));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configManager.IsCVarRegistered(name))
|
||||
{
|
||||
shell.WriteError($"CVar '{name}' is not registered. Use 'cvar ?' to get a list of all registered CVars.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 1)
|
||||
{
|
||||
// Read CVar
|
||||
var value = configManager.GetCVar<object>(name);
|
||||
shell.WriteLine(value.ToString() ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write CVar
|
||||
var value = args[1];
|
||||
var type = configManager.GetCVarType(name);
|
||||
try
|
||||
{
|
||||
var parsed = ParseObject(type, value);
|
||||
configManager.SetCVar(name, parsed);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
shell.WriteLine($"Input value is in incorrect format for type {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UsedImplicitly]
|
||||
public sealed class SaveConfig : IConsoleCommand
|
||||
{
|
||||
|
||||
@@ -739,103 +739,6 @@ namespace Robust.Client.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gc";
|
||||
public string Description => "Run the GC.";
|
||||
public string Help => "gc [generation]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
GC.Collect();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(args[0], out int result))
|
||||
GC.Collect(result);
|
||||
else
|
||||
shell.WriteError("Failed to parse argument.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcFullCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gcf";
|
||||
public string Description => "Run the GC, fully, compacting LOH and everything.";
|
||||
public string Help => "gcf";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect(2, GCCollectionMode.Forced, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcModeCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "gc_mode";
|
||||
|
||||
public string Description => "Change/Read the GC Latency mode.";
|
||||
|
||||
public string Help => "gc_mode\nSee current GC Latencymode\ngc_mode [type]\n Change GC Latency mode to [type]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var prevMode = GCSettings.LatencyMode;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteLine($"current gc latency mode: {(int) prevMode} ({prevMode})");
|
||||
shell.WriteLine("possible modes:");
|
||||
foreach (int mode in (int[]) Enum.GetValues(typeof(GCLatencyMode)))
|
||||
{
|
||||
shell.WriteLine($" {mode}: {Enum.GetName(typeof(GCLatencyMode), mode)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GCLatencyMode mode;
|
||||
if (char.IsDigit(args[0][0]) && int.TryParse(args[0], out var modeNum))
|
||||
{
|
||||
mode = (GCLatencyMode) modeNum;
|
||||
}
|
||||
else if (!Enum.TryParse(args[0], true, out mode))
|
||||
{
|
||||
shell.WriteLine($"unknown gc latency mode: {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine($"attempting gc latency mode change: {(int) prevMode} ({prevMode}) -> {(int) mode} ({mode})");
|
||||
GCSettings.LatencyMode = mode;
|
||||
shell.WriteLine($"resulting gc latency mode: {(int) GCSettings.LatencyMode} ({GCSettings.LatencyMode})");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class SerializeStatsCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "szr_stats";
|
||||
|
||||
public string Description => "Report serializer statistics.";
|
||||
|
||||
public string Help => "szr_stats";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
|
||||
shell.WriteLine($"serialized: {RobustSerializer.BytesSerialized} bytes, {RobustSerializer.ObjectsSerialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectSerializedBytes} bytes, {RobustSerializer.LargestObjectSerializedType} objects");
|
||||
shell.WriteLine($"deserialized: {RobustSerializer.BytesDeserialized} bytes, {RobustSerializer.ObjectsDeserialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectDeserializedBytes} bytes, {RobustSerializer.LargestObjectDeserializedType} objects");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class ChunkInfoCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "chunkinfo";
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Network;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
sealed class HelpCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "help";
|
||||
public string Help => "When no arguments are provided, displays a generic help text. When an argument is passed, display the help text for the command with that name.";
|
||||
public string Description => "Display help text.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
shell.WriteLine("To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'.");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
string commandname = args[0];
|
||||
if (!shell.ConsoleHost.RegisteredCommands.ContainsKey(commandname))
|
||||
{
|
||||
if (!IoCManager.Resolve<IClientNetManager>().IsConnected)
|
||||
{
|
||||
// No server so nothing to respond with unknown command.
|
||||
shell.WriteError("Unknown command: " + commandname);
|
||||
return;
|
||||
}
|
||||
// TODO: Maybe have a server side help?
|
||||
return;
|
||||
}
|
||||
IConsoleCommand command = shell.ConsoleHost.RegisteredCommands[commandname];
|
||||
shell.WriteLine(string.Format("{0} - {1}", command.Command, command.Description));
|
||||
shell.WriteLine(command.Help);
|
||||
break;
|
||||
|
||||
default:
|
||||
shell.WriteError("Invalid amount of arguments.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ListCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "list";
|
||||
public string Help => "Usage: list [filter]\n" +
|
||||
"Lists all available commands, and their short descriptions.\n" +
|
||||
"If a filter is provided, " +
|
||||
"only commands that contain the given string in their name will be listed.";
|
||||
public string Description => "List all commands, optionally with a filter.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var filter = "";
|
||||
if (args.Length == 1)
|
||||
{
|
||||
filter = args[0];
|
||||
}
|
||||
|
||||
var conGroup = IoCManager.Resolve<IClientConGroupController>();
|
||||
foreach (var command in shell.ConsoleHost.RegisteredCommands.Values
|
||||
.Where(p => p.Command.Contains(filter) && (p is not ServerDummyCommand || conGroup.CanCommand(p.Command)))
|
||||
.OrderBy(c => c.Command))
|
||||
{
|
||||
shell.WriteLine(command.Command + ": " + command.Description);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Runtime.Loader;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class ListAssembliesCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "lsasm";
|
||||
public string Description => "Lists loaded assemblies by load context.";
|
||||
public string Help => Command;
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
foreach (var context in AssemblyLoadContext.All)
|
||||
{
|
||||
shell.WriteLine($"{context.Name}:");
|
||||
foreach (var assembly in context.Assemblies.OrderBy(a => a.FullName))
|
||||
{
|
||||
shell.WriteLine($" {assembly.FullName}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using Robust.Shared.Log;
|
||||
using System;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Client.Console.Commands
|
||||
{
|
||||
sealed class LogSetLevelCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "loglevel";
|
||||
public string Description => "Changes the log level for a provided sawmill.";
|
||||
public string Help => "Usage: loglevel <sawmill> <level>"
|
||||
+ "\n sawmill: A label prefixing log messages. This is the one you're setting the level for."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
LogLevel? level;
|
||||
if (levelname == "null")
|
||||
{
|
||||
level = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
level = result;
|
||||
}
|
||||
Logger.GetSawmill(name).Level = level;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TestLog : IConsoleCommand
|
||||
{
|
||||
public string Command => "testlog";
|
||||
public string Description => "Writes a test log to a sawmill.";
|
||||
public string Help => "Usage: testlog <sawmill> <level> <message>"
|
||||
+ "\n sawmill: A label prefixing the logged message."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum."
|
||||
+ "\n message: The message to be logged. Wrap this in double quotes if you want to use spaces.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 3 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
var message = args[2]; // yes this doesn't support spaces idgaf.
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
var level = result;
|
||||
|
||||
Logger.LogS(level, name, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
@@ -15,5 +18,7 @@ namespace Robust.Client.Console
|
||||
event EventHandler<AddFormattedMessageArgs> AddFormatted;
|
||||
|
||||
void AddFormattedLine(FormattedMessage message);
|
||||
|
||||
Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,8 @@ namespace Robust.Client.Input
|
||||
common.AddFunction(EngineKeyFunctions.TextScrollToBottom);
|
||||
common.AddFunction(EngineKeyFunctions.TextDelete);
|
||||
common.AddFunction(EngineKeyFunctions.TextTabComplete);
|
||||
common.AddFunction(EngineKeyFunctions.TextCompleteNext);
|
||||
common.AddFunction(EngineKeyFunctions.TextCompletePrev);
|
||||
|
||||
var editor = contexts.New("editor", common);
|
||||
editor.AddFunction(EngineKeyFunctions.EditorLinePlace);
|
||||
|
||||
@@ -281,6 +281,8 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
public event Action<GUITextEventArgs>? OnTextTyped;
|
||||
|
||||
protected internal override void TextEntered(GUITextEventArgs args)
|
||||
{
|
||||
base.TextEntered(args);
|
||||
@@ -297,8 +299,11 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
|
||||
InsertAtCursor(args.AsRune.ToString());
|
||||
OnTextTyped?.Invoke(args);
|
||||
}
|
||||
|
||||
public event Action<LineEditBackspaceEventArgs>? OnBackspace;
|
||||
|
||||
protected internal override void KeyBindDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
base.KeyBindDown(args);
|
||||
@@ -315,6 +320,9 @@ namespace Robust.Client.UserInterface.Controls
|
||||
if (Editable)
|
||||
{
|
||||
var changed = false;
|
||||
var oldText = _text;
|
||||
var cursor = _cursorPosition;
|
||||
var selectStart = _selectionStart;
|
||||
if (_selectionStart != _cursorPosition)
|
||||
{
|
||||
_text = _text.Remove(SelectionLower, SelectionLength);
|
||||
@@ -341,6 +349,7 @@ namespace Robust.Client.UserInterface.Controls
|
||||
_selectionStart = _cursorPosition;
|
||||
OnTextChanged?.Invoke(new LineEditEventArgs(this, _text));
|
||||
_updatePseudoClass();
|
||||
OnBackspace?.Invoke(new LineEditBackspaceEventArgs(oldText, _text, cursor, selectStart));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -654,6 +663,35 @@ namespace Robust.Client.UserInterface.Controls
|
||||
return index;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get offset from the left of the control
|
||||
/// to the left edge of the text glyph at the specified index in the text.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The returned value can be outside the bounds of the control if the glyph is currently clipped off.
|
||||
/// </remarks>
|
||||
public float GetOffsetAtIndex(int index)
|
||||
{
|
||||
var style = _getStyleBox();
|
||||
var contentBox = style.GetContentBox(PixelSizeBox);
|
||||
|
||||
var font = _getFont();
|
||||
var i = 0;
|
||||
var chrPosX = contentBox.Left - _drawOffset;
|
||||
foreach (var rune in _text.EnumerateRunes())
|
||||
{
|
||||
if (i >= index)
|
||||
break;
|
||||
|
||||
if (font.TryGetCharMetrics(rune, UIScale, out var metrics))
|
||||
chrPosX += metrics.Advance;
|
||||
|
||||
i += rune.Utf16SequenceLength;
|
||||
}
|
||||
|
||||
return chrPosX / UIScale;
|
||||
}
|
||||
|
||||
protected internal override void KeyboardFocusEntered()
|
||||
{
|
||||
base.KeyboardFocusEntered();
|
||||
@@ -822,6 +860,26 @@ namespace Robust.Client.UserInterface.Controls
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class LineEditBackspaceEventArgs : EventArgs
|
||||
{
|
||||
public string OldText { get; }
|
||||
public string NewText { get; }
|
||||
public int OldCursorPosition { get; }
|
||||
public int OldSelectionStart { get; }
|
||||
|
||||
public LineEditBackspaceEventArgs(
|
||||
string oldText,
|
||||
string newText,
|
||||
int oldCursorPosition,
|
||||
int oldSelectionStart)
|
||||
{
|
||||
OldText = oldText;
|
||||
NewText = newText;
|
||||
OldCursorPosition = oldCursorPosition;
|
||||
OldSelectionStart = oldSelectionStart;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Use a separate control to do the rendering to make use of RectClipContent,
|
||||
/// so that we can clip characters in half.
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Shared;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.Maths;
|
||||
using Robust.Shared.Utility;
|
||||
using Robust.Shared.Utility.Collections;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
public sealed partial class DebugConsole
|
||||
{
|
||||
private readonly DebugConsoleCompletion _compPopup;
|
||||
|
||||
// Last valid completion result we got.
|
||||
private CompletionResult? _compCurResult;
|
||||
|
||||
// The parameter count for the above completion result.
|
||||
// Used to immediately invalidate it if the amount changes.
|
||||
private int _compParamCount;
|
||||
|
||||
// The filtered set of completions currently shown to the user.
|
||||
private CompletionOption[]? _compFiltered;
|
||||
|
||||
// Which completion is currently selected, index into _compFiltered.
|
||||
private int _compSelected;
|
||||
|
||||
// Vertical scroll offset of the completion list.
|
||||
private int _compVerticalOffset;
|
||||
|
||||
// Used for sequencing to nicely handle out-of-order completion responses.
|
||||
private int _compSeqSend;
|
||||
private int _compSeqRecv;
|
||||
|
||||
|
||||
private CancellationTokenSource _compCancel = new();
|
||||
|
||||
private void InitCompletions()
|
||||
{
|
||||
CommandBar.OnTextTyped += CommandBarOnOnTextTyped;
|
||||
CommandBar.OnFocusExit += CommandBarOnOnFocusExit;
|
||||
CommandBar.OnBackspace += CommandBarOnOnBackspace;
|
||||
}
|
||||
|
||||
private void CommandBarOnOnFocusExit(LineEdit.LineEditEventArgs obj)
|
||||
{
|
||||
AbortActiveCompletions();
|
||||
}
|
||||
|
||||
private void CommandBarOnOnTextTyped(GUITextEventArgs obj)
|
||||
{
|
||||
TypeUpdateCompletions(true);
|
||||
}
|
||||
|
||||
private void CommandBarOnOnBackspace(LineEdit.LineEditBackspaceEventArgs eventArgs)
|
||||
{
|
||||
if (eventArgs.OldCursorPosition == 0 || eventArgs.OldSelectionStart != eventArgs.OldCursorPosition)
|
||||
{
|
||||
AbortActiveCompletions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (CommandBar.CursorPosition == 0)
|
||||
{
|
||||
// Don't do completions if you have nothing typed.
|
||||
AbortActiveCompletions();
|
||||
return;
|
||||
}
|
||||
|
||||
TypeUpdateCompletions(true);
|
||||
}
|
||||
|
||||
|
||||
private void AbortActiveCompletions()
|
||||
{
|
||||
_compCurResult = null;
|
||||
_compFiltered = null;
|
||||
_compSelected = 0;
|
||||
_compVerticalOffset = 0;
|
||||
_compPopup.Close();
|
||||
|
||||
_compCancel.Cancel();
|
||||
_compCancel.Dispose();
|
||||
_compCancel = new CancellationTokenSource();
|
||||
}
|
||||
|
||||
private async void TypeUpdateCompletions(bool fullUpdate)
|
||||
{
|
||||
var (args, _, _) = CalcTypingArgs();
|
||||
|
||||
if (args.Count != _compParamCount)
|
||||
{
|
||||
_compParamCount = args.Count;
|
||||
AbortActiveCompletions();
|
||||
}
|
||||
|
||||
if (fullUpdate)
|
||||
{
|
||||
var seq = ++_compSeqSend;
|
||||
var task = _consoleHost.GetCompletions(args, _compCancel.Token);
|
||||
|
||||
if (!task.IsCompleted)
|
||||
{
|
||||
// If we don't immediately get a result from the console (e.g. server command),
|
||||
// we update the filtered immediately before asynchronously waiting on it.
|
||||
UpdateFilteredCompletions();
|
||||
|
||||
// This means we only update completions once when running synchronously.
|
||||
}
|
||||
|
||||
CompletionResult result;
|
||||
try
|
||||
{
|
||||
result = await task;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (seq < _compSeqRecv)
|
||||
{
|
||||
// Newer result already came before us.
|
||||
return;
|
||||
}
|
||||
|
||||
_compSeqRecv = seq;
|
||||
_compCurResult = result;
|
||||
}
|
||||
|
||||
UpdateFilteredCompletions();
|
||||
}
|
||||
|
||||
private void UpdateFilteredCompletions()
|
||||
{
|
||||
if (_compCurResult == null)
|
||||
return;
|
||||
|
||||
var (_, curTyping, _) = CalcTypingArgs();
|
||||
|
||||
var curSelected = _compFiltered?.Length > 0 ? _compFiltered[_compSelected] : default;
|
||||
_compFiltered = FilterCompletions(_compCurResult.Options, curTyping);
|
||||
if (curSelected == default)
|
||||
{
|
||||
_compSelected = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
var foundIdx = Array.IndexOf(_compFiltered, curSelected);
|
||||
_compSelected = foundIdx > 0 ? foundIdx : 0;
|
||||
}
|
||||
|
||||
FixScrollMargins();
|
||||
|
||||
// Logger.Debug($"Filtered completions: {string.Join(", ", _compFiltered)}");
|
||||
|
||||
UpdateCompletionsPopup();
|
||||
}
|
||||
|
||||
private void UpdateCompletionsPopup()
|
||||
{
|
||||
if (_compFiltered == null)
|
||||
return;
|
||||
|
||||
DebugTools.AssertNotNull(_compCurResult);
|
||||
|
||||
var (_, _, endRange) = CalcTypingArgs();
|
||||
|
||||
var offset = CommandBar.GetOffsetAtIndex(endRange.start);
|
||||
// Logger.Debug($"Offset: {offset}");
|
||||
|
||||
_compPopup.Close();
|
||||
|
||||
_compPopup.Contents.RemoveAllChildren();
|
||||
|
||||
if (_compCurResult!.Hint != null)
|
||||
{
|
||||
var hint = _compCurResult.Hint;
|
||||
_compPopup.Contents.AddChild(new Label
|
||||
{
|
||||
Text = hint,
|
||||
FontColorOverride = Color.Gray
|
||||
});
|
||||
}
|
||||
|
||||
// Fill out list completions.
|
||||
var maxCount = _cfg.GetCVar(CVars.ConCompletionCount);
|
||||
var c = 0;
|
||||
for (var i = _compVerticalOffset; i < _compFiltered.Length && c < maxCount; i++, c++)
|
||||
{
|
||||
var (value, hint, _) = _compFiltered[i];
|
||||
|
||||
var labelValue = new Label
|
||||
{
|
||||
Text = value,
|
||||
FontColorOverride = i == _compSelected ? Color.White : Color.DarkGray
|
||||
};
|
||||
|
||||
if (hint != null)
|
||||
{
|
||||
_compPopup.Contents.AddChild(new BoxContainer
|
||||
{
|
||||
Orientation = BoxContainer.LayoutOrientation.Horizontal,
|
||||
Children =
|
||||
{
|
||||
labelValue,
|
||||
new Label
|
||||
{
|
||||
Text = $" - {hint}",
|
||||
FontColorOverride = Color.Gray
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
_compPopup.Contents.AddChild(labelValue);
|
||||
}
|
||||
}
|
||||
|
||||
if (_compPopup.Contents.ChildCount != 0)
|
||||
{
|
||||
_compPopup.Open(
|
||||
UIBox2.FromDimensions(
|
||||
offset - _compPopup.Contents.Margin.Left, CommandBar.GlobalPosition.Y + CommandBar.Height + 2,
|
||||
5, 5));
|
||||
}
|
||||
}
|
||||
|
||||
private (List<string> args, string curTyping, (int start, int end) lastRange) CalcTypingArgs()
|
||||
{
|
||||
var cursor = CommandBar.CursorPosition;
|
||||
// Don't consider text after the cursor.
|
||||
var text = CommandBar.Text.AsSpan(0, cursor);
|
||||
|
||||
var args = new List<string>();
|
||||
var ranges = new ValueList<(int start, int end)>();
|
||||
CommandParsing.ParseArguments(text, args, ref ranges);
|
||||
|
||||
if (args.Count == 0 || ranges[^1].end != text.Length)
|
||||
args.Add("");
|
||||
|
||||
(int, int) lastRange;
|
||||
if (ranges.Count == 0)
|
||||
lastRange = default;
|
||||
else if (ranges.Count == args.Count)
|
||||
lastRange = ranges[^1];
|
||||
else
|
||||
lastRange = (cursor, cursor);
|
||||
|
||||
return (args, args[^1], lastRange);
|
||||
}
|
||||
|
||||
private CompletionOption[] FilterCompletions(IEnumerable<CompletionOption> completions, string curTyping)
|
||||
{
|
||||
return completions
|
||||
.Where(c => c.Value.StartsWith(curTyping, StringComparison.CurrentCultureIgnoreCase))
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private void CompletionKeyDown(GUIBoundKeyEventArgs args)
|
||||
{
|
||||
if (args.Function == EngineKeyFunctions.TextTabComplete)
|
||||
{
|
||||
if (_compFiltered != null && _compSelected < _compFiltered.Length)
|
||||
{
|
||||
// Figure out typing word so we know how much to replace.
|
||||
var (completion, _, completionFlags) = _compFiltered[_compSelected];
|
||||
var (_, _, lastRange) = CalcTypingArgs();
|
||||
|
||||
// Replace the full word from the start.
|
||||
// This means that letter casing will match the completion suggestion.
|
||||
CommandBar.CursorPosition = lastRange.end;
|
||||
CommandBar.SelectionStart = lastRange.start;
|
||||
var insertValue = CommandParsing.Escape(completion);
|
||||
if ((completionFlags & CompletionOptionFlags.PartialCompletion) == 0)
|
||||
insertValue += " ";
|
||||
|
||||
CommandBar.InsertAtCursor(insertValue);
|
||||
|
||||
TypeUpdateCompletions(true);
|
||||
|
||||
args.Handle();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == EngineKeyFunctions.TextCompleteNext)
|
||||
{
|
||||
if (_compFiltered == null)
|
||||
return;
|
||||
|
||||
args.Handle();
|
||||
var len = _compFiltered.Length;
|
||||
var pos = (_compSelected + 1) % len;
|
||||
_compSelected = pos;
|
||||
|
||||
FixScrollMargins();
|
||||
|
||||
UpdateCompletionsPopup();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Function == EngineKeyFunctions.TextCompletePrev)
|
||||
{
|
||||
if (_compFiltered == null)
|
||||
return;
|
||||
|
||||
args.Handle();
|
||||
var len = _compFiltered.Length;
|
||||
var pos = MathHelper.Mod(_compSelected - 1, len);
|
||||
_compSelected = pos;
|
||||
|
||||
FixScrollMargins();
|
||||
|
||||
UpdateCompletionsPopup();
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private void FixScrollMargins()
|
||||
{
|
||||
if (_compFiltered == null)
|
||||
return;
|
||||
|
||||
var maxCount = _cfg.GetCVar(CVars.ConCompletionCount);
|
||||
var showCount = Math.Min(maxCount, _compFiltered.Length);
|
||||
var margin = _cfg.GetCVar(CVars.ConCompletionMargin);
|
||||
|
||||
var posBottom = showCount + _compVerticalOffset - margin;
|
||||
if (_compSelected >= posBottom)
|
||||
_compVerticalOffset = Math.Min(_compFiltered.Length - showCount, _compSelected + 1 + margin - showCount);
|
||||
|
||||
if (_compSelected < _compVerticalOffset + margin)
|
||||
_compVerticalOffset = Math.Max(0, _compSelected - margin);
|
||||
}
|
||||
|
||||
private void CompletionCommandEntered()
|
||||
{
|
||||
AbortActiveCompletions();
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,13 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.Console;
|
||||
using Robust.Client.Graphics;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Input;
|
||||
using Robust.Shared.IoC;
|
||||
@@ -44,21 +43,20 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
[Dependency] private readonly IClientConsoleHost _consoleHost = default!;
|
||||
[Dependency] private readonly IResourceManager _resourceManager = default!;
|
||||
[Dependency] private readonly IConfigurationManager _cfg = default!;
|
||||
|
||||
private static readonly ResourcePath HistoryPath = new("/debug_console_history.json");
|
||||
|
||||
private readonly ConcurrentQueue<FormattedMessage> _messageQueue = new();
|
||||
|
||||
private bool commandChanged = true;
|
||||
private readonly List<string> searchResults;
|
||||
private int searchIndex = 0;
|
||||
|
||||
public DebugConsole()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
|
||||
IoCManager.InjectDependencies(this);
|
||||
|
||||
InitCompletions();
|
||||
|
||||
CommandBar.OnTextChanged += OnCommandChanged;
|
||||
CommandBar.OnKeyBindDown += CommandBarOnOnKeyBindDown;
|
||||
CommandBar.OnTextEntered += CommandEntered;
|
||||
@@ -66,7 +64,7 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
|
||||
_loadHistoryFromDisk();
|
||||
|
||||
searchResults = new List<string>();
|
||||
_compPopup = new DebugConsoleCompletion();
|
||||
}
|
||||
|
||||
protected override void EnteredTree()
|
||||
@@ -76,6 +74,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString += OnAddString;
|
||||
_consoleHost.AddFormatted += OnAddFormatted;
|
||||
_consoleHost.ClearText += OnClearText;
|
||||
|
||||
UserInterfaceManager.ModalRoot.AddChild(_compPopup);
|
||||
}
|
||||
|
||||
protected override void ExitedTree()
|
||||
@@ -85,6 +85,8 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
_consoleHost.AddString -= OnAddString;
|
||||
_consoleHost.AddFormatted -= OnAddFormatted;
|
||||
_consoleHost.ClearText -= OnClearText;
|
||||
|
||||
UserInterfaceManager.ModalRoot.RemoveChild(_compPopup);
|
||||
}
|
||||
|
||||
private void OnClearText(object? _, EventArgs args)
|
||||
@@ -120,9 +122,11 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
_consoleHost.ExecuteCommand(args.Text);
|
||||
CommandBar.Clear();
|
||||
|
||||
CompletionCommandEntered();
|
||||
}
|
||||
|
||||
commandChanged = true;
|
||||
// commandChanged = true;
|
||||
}
|
||||
|
||||
private void OnHistoryChanged()
|
||||
@@ -173,80 +177,15 @@ namespace Robust.Client.UserInterface.CustomControls
|
||||
{
|
||||
Output.ScrollToBottom();
|
||||
args.Handle();
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.GuiTabNavigateNext)
|
||||
{
|
||||
NextCommand();
|
||||
args.Handle();
|
||||
}
|
||||
else if (args.Function == EngineKeyFunctions.GuiTabNavigatePrev)
|
||||
{
|
||||
PrevCommand();
|
||||
args.Handle();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetInput(string cmd)
|
||||
{
|
||||
CommandBar.Text = cmd;
|
||||
CommandBar.CursorPosition = cmd.Length;
|
||||
}
|
||||
|
||||
private void FindCommands()
|
||||
{
|
||||
searchResults.Clear();
|
||||
searchIndex = 0;
|
||||
commandChanged = false;
|
||||
foreach (var cmd in _consoleHost.RegisteredCommands)
|
||||
{
|
||||
if (cmd.Key.StartsWith(CommandBar.Text))
|
||||
{
|
||||
searchResults.Add(cmd.Key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void NextCommand()
|
||||
{
|
||||
if (!commandChanged)
|
||||
{
|
||||
if (searchResults.Count == 0)
|
||||
return;
|
||||
|
||||
searchIndex = (searchIndex + 1) % searchResults.Count;
|
||||
SetInput(searchResults[searchIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
FindCommands();
|
||||
if (searchResults.Count == 0)
|
||||
return;
|
||||
|
||||
SetInput(searchResults[0]);
|
||||
}
|
||||
|
||||
private void PrevCommand()
|
||||
{
|
||||
if (!commandChanged)
|
||||
{
|
||||
if (searchResults.Count == 0)
|
||||
return;
|
||||
|
||||
searchIndex = MathHelper.Mod(searchIndex - 1, searchResults.Count);
|
||||
SetInput(searchResults[searchIndex]);
|
||||
return;
|
||||
}
|
||||
|
||||
FindCommands();
|
||||
if (searchResults.Count == 0)
|
||||
return;
|
||||
|
||||
SetInput(searchResults[^1]);
|
||||
CompletionKeyDown(args);
|
||||
}
|
||||
|
||||
private void OnCommandChanged(LineEdit.LineEditEventArgs args)
|
||||
{
|
||||
commandChanged = true;
|
||||
// commandChanged = true;
|
||||
}
|
||||
|
||||
private async void _loadHistoryFromDisk()
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<Popup xmlns="https://spacestation14.io"
|
||||
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
|
||||
<PanelContainer>
|
||||
<PanelContainer.PanelOverride>
|
||||
<gfx:StyleBoxFlat BackgroundColor="#000D" />
|
||||
</PanelContainer.PanelOverride>
|
||||
<BoxContainer Access="Public" Margin="2" Name="Contents" Orientation="Vertical" />
|
||||
</PanelContainer>
|
||||
</Popup>
|
||||
@@ -0,0 +1,14 @@
|
||||
using Robust.Client.AutoGenerated;
|
||||
using Robust.Client.UserInterface.Controls;
|
||||
using Robust.Client.UserInterface.XAML;
|
||||
|
||||
namespace Robust.Client.UserInterface.CustomControls;
|
||||
|
||||
[GenerateTypedNameReferences]
|
||||
internal sealed partial class DebugConsoleCompletion : Popup
|
||||
{
|
||||
public DebugConsoleCompletion()
|
||||
{
|
||||
RobustXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
using System;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class CVarCommand : SharedCVarCommand, IConsoleCommand
|
||||
{
|
||||
public override void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length < 1 || args.Length > 2)
|
||||
{
|
||||
shell.WriteLine("Must provide exactly one or two arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
var name = args[0];
|
||||
|
||||
if (name == "?")
|
||||
{
|
||||
var cvars = configManager.GetRegisteredCVars();
|
||||
shell.WriteLine(string.Join("\n", cvars));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configManager.IsCVarRegistered(name))
|
||||
{
|
||||
shell.WriteLine($"CVar '{name}' is not registered. Use 'cvar ?' to get a list of all registered CVars.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 1)
|
||||
{
|
||||
// Read CVar
|
||||
var value = configManager.GetCVar<object>(name);
|
||||
shell.WriteLine(value.ToString()!);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write CVar
|
||||
var value = args[1];
|
||||
var type = configManager.GetCVarType(name);
|
||||
try
|
||||
{
|
||||
var parsed = ParseObject(type, value);
|
||||
configManager.SetCVar(name, parsed);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
shell.WriteLine($"Input value is in incorrect format for type {type}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
[UsedImplicitly]
|
||||
internal sealed class ListAssembliesCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "lsasm";
|
||||
public string Description => "Lists loaded assemblies by load context.";
|
||||
public string Help => Command;
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var context in AssemblyLoadContext.All)
|
||||
{
|
||||
sb.AppendFormat("{0}:\n", context.Name);
|
||||
foreach (var assembly in context.Assemblies)
|
||||
{
|
||||
sb.AppendFormat(" {0}\n", assembly.FullName);
|
||||
}
|
||||
}
|
||||
|
||||
shell.WriteLine(sb.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
public sealed class ListCommands : IConsoleCommand
|
||||
{
|
||||
public string Command => "list";
|
||||
|
||||
public string Description => "Outputs a list of all commands which are currently available to you. " +
|
||||
"If a filter is provided, " +
|
||||
"only commands that contain the given string in their name will be listed.";
|
||||
|
||||
public string Help => "Usage: list [filter]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var filter = "";
|
||||
if (args.Length == 1)
|
||||
{
|
||||
filter = args[0];
|
||||
}
|
||||
|
||||
var builder = new StringBuilder("SIDE NAME DESC\n-------------------------\n");
|
||||
foreach (var command in shell.ConsoleHost.RegisteredCommands.Values
|
||||
.Where(p => p.Command.Contains(filter))
|
||||
.OrderBy(c => c.Command))
|
||||
{
|
||||
//TODO: Make this actually check permissions.
|
||||
|
||||
builder.AppendLine($"S {command.Command,-16}{command.Description}");
|
||||
}
|
||||
|
||||
var message = builder.ToString().Trim(' ', '\n');
|
||||
shell.WriteLine(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
using System;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Server.Console.Commands
|
||||
{
|
||||
sealed class LogSetLevelCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "loglevel";
|
||||
public string Description => "Changes the log level for a provided sawmill.";
|
||||
public string Help => "Usage: loglevel <sawmill> <level>"
|
||||
+ "\n sawmill: A label prefixing log messages. This is the one you're setting the level for."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteLine("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
LogLevel? level;
|
||||
if (levelname == "null")
|
||||
{
|
||||
level = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
level = result;
|
||||
}
|
||||
Logger.GetSawmill(name).Level = level;
|
||||
}
|
||||
}
|
||||
|
||||
sealed class TestLog : IConsoleCommand
|
||||
{
|
||||
public string Command => "testlog";
|
||||
public string Description => "Writes a test log to a sawmill.";
|
||||
public string Help => "Usage: testlog <sawmill> <level> <message>"
|
||||
+ "\n sawmill: A label prefixing the logged message."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum."
|
||||
+ "\n message: The message to be logged. Wrap this in double quotes if you want to use spaces.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3)
|
||||
{
|
||||
shell.WriteLine("Invalid argument amount. Expected exactly 3 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
var message = args[2]; // yes this doesn't support spaces idgaf.
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
var level = result;
|
||||
|
||||
Logger.LogS(level, name, message, level);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,5 +212,23 @@ namespace Robust.Server.Console.Commands
|
||||
network.DisconnectChannel(target.ConnectedClient, reason);
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var playerManager = IoCManager.Resolve<IPlayerManager>();
|
||||
var options = playerManager.ServerSessions.OrderBy(c => c.Name).Select(c => c.Name).ToArray();
|
||||
|
||||
return CompletionResult.FromHintOptions(options, "<PlayerIndex>");
|
||||
}
|
||||
|
||||
if (args.Length > 1)
|
||||
{
|
||||
return CompletionResult.FromHint("[<Reason>]");
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
using System;
|
||||
using System.Runtime;
|
||||
using System.Text;
|
||||
using Robust.Shared.Configuration;
|
||||
using Robust.Shared.Console;
|
||||
@@ -72,41 +70,6 @@ namespace Robust.Server.Console.Commands
|
||||
}
|
||||
}
|
||||
|
||||
sealed class HelpCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "help";
|
||||
|
||||
public string Description =>
|
||||
"When no arguments are provided, displays a generic help text. When an argument is passed, display the help text for the command with that name.";
|
||||
|
||||
public string Help => "Help";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
shell.WriteLine("To display help for a specific command, write 'help <command>'. To list all available commands, write 'list'.");
|
||||
break;
|
||||
|
||||
case 1:
|
||||
var commandName = args[0];
|
||||
if (!shell.ConsoleHost.RegisteredCommands.TryGetValue(commandName, out var cmd))
|
||||
{
|
||||
shell.WriteLine($"Unknown command: {commandName}");
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine($"Use: {cmd.Help}\n{cmd.Description}");
|
||||
break;
|
||||
|
||||
default:
|
||||
shell.WriteLine("Invalid amount of arguments.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ShowTimeCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "showtime";
|
||||
@@ -119,115 +82,4 @@ namespace Robust.Server.Console.Commands
|
||||
shell.WriteLine($"Paused: {timing.Paused}, CurTick: {timing.CurTick}, CurTime: {timing.CurTime}, RealTime: {timing.RealTime}");
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gc";
|
||||
public string Description => "Run the GC.";
|
||||
public string Help => "gc [generation]";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
GC.Collect();
|
||||
}
|
||||
else
|
||||
{
|
||||
if(int.TryParse(args[0], out int result))
|
||||
GC.Collect(result);
|
||||
else
|
||||
shell.WriteLine("Failed to parse argument.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcModeCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "gc_mode";
|
||||
|
||||
public string Description => "Change the GC Latency mode.";
|
||||
|
||||
public string Help => "gc_mode [type]";
|
||||
|
||||
public void Execute(IConsoleShell console, string argStr, string[] args)
|
||||
{
|
||||
var prevMode = GCSettings.LatencyMode;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
console.WriteLine($"current gc latency mode: {(int) prevMode} ({prevMode})");
|
||||
console.WriteLine("possible modes:");
|
||||
foreach (var mode in (int[]) Enum.GetValues(typeof(GCLatencyMode)))
|
||||
{
|
||||
console.WriteLine($" {mode}: {Enum.GetName(typeof(GCLatencyMode), mode)}");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
GCLatencyMode mode;
|
||||
if (char.IsDigit(args[0][0]) && int.TryParse(args[0], out var modeNum))
|
||||
{
|
||||
mode = (GCLatencyMode) modeNum;
|
||||
}
|
||||
else if (!Enum.TryParse(args[0], true, out mode))
|
||||
{
|
||||
console.WriteLine($"unknown gc latency mode: {args[0]}");
|
||||
return;
|
||||
}
|
||||
|
||||
console.WriteLine($"attempting gc latency mode change: {(int) prevMode} ({prevMode}) -> {(int) mode} ({mode})");
|
||||
GCSettings.LatencyMode = mode;
|
||||
console.WriteLine($"resulting gc latency mode: {(int) GCSettings.LatencyMode} ({GCSettings.LatencyMode})");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class SerializeStatsCommand : IConsoleCommand
|
||||
{
|
||||
|
||||
public string Command => "szr_stats";
|
||||
|
||||
public string Description => "Report serializer statistics.";
|
||||
|
||||
public string Help => "szr_stats";
|
||||
|
||||
public void Execute(IConsoleShell console, string argStr, string[] args)
|
||||
{
|
||||
|
||||
console.WriteLine($"serialized: {RobustSerializer.BytesSerialized} bytes, {RobustSerializer.ObjectsSerialized} objects");
|
||||
console.WriteLine($"largest serialized: {RobustSerializer.LargestObjectSerializedBytes} bytes, {RobustSerializer.LargestObjectSerializedType} objects");
|
||||
console.WriteLine($"deserialized: {RobustSerializer.BytesDeserialized} bytes, {RobustSerializer.ObjectsDeserialized} objects");
|
||||
console.WriteLine($"largest serialized: {RobustSerializer.LargestObjectDeserializedBytes} bytes, {RobustSerializer.LargestObjectDeserializedType} objects");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal sealed class MemCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "mem";
|
||||
public string Description => "prints memory info";
|
||||
public string Help => "mem";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
#if !NETCOREAPP
|
||||
shell.SendText(player, "Memory info is only available on .NET Core");
|
||||
#else
|
||||
var info = GC.GetGCMemoryInfo();
|
||||
|
||||
shell.WriteLine($@"Heap Size: {FormatBytes(info.HeapSizeBytes)} Total Allocated: {FormatBytes(GC.GetTotalMemory(false))}");
|
||||
#endif
|
||||
}
|
||||
|
||||
#if NETCOREAPP
|
||||
private static string FormatBytes(long bytes)
|
||||
{
|
||||
return $"{bytes / 1024} KiB";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Server.Player;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.Exceptions;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Network;
|
||||
using Robust.Shared.Network.Messages;
|
||||
using Robust.Shared.Players;
|
||||
@@ -12,7 +14,7 @@ using Robust.Shared.Utility;
|
||||
namespace Robust.Server.Console
|
||||
{
|
||||
/// <inheritdoc cref="IServerConsoleHost" />
|
||||
internal sealed class ServerConsoleHost : ConsoleHost, IServerConsoleHost
|
||||
internal sealed class ServerConsoleHost : ConsoleHost, IServerConsoleHost, IConsoleHostInternal
|
||||
{
|
||||
[Dependency] private readonly IConGroupController _groupController = default!;
|
||||
[Dependency] private readonly IPlayerManager _players = default!;
|
||||
@@ -56,14 +58,26 @@ namespace Robust.Server.Console
|
||||
OutputText(null, text, true);
|
||||
}
|
||||
|
||||
public bool IsCmdServer(IConsoleCommand cmd) => true;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Initialize()
|
||||
{
|
||||
RegisterCommand("sudo", "sudo make me a sandwich", "sudo <command>",(shell, argStr, _) =>
|
||||
RegisterCommand("sudo", "sudo make me a sandwich", "sudo <command>", (shell, argStr, _) =>
|
||||
{
|
||||
var localShell = shell.ConsoleHost.LocalShell;
|
||||
var sudoShell = new SudoShell(this, localShell, shell);
|
||||
ExecuteInShell(sudoShell, argStr.Substring("sudo ".Length));
|
||||
ExecuteInShell(sudoShell, argStr["sudo ".Length..]);
|
||||
}, (shell, args) =>
|
||||
{
|
||||
var localShell = shell.ConsoleHost.LocalShell;
|
||||
var sudoShell = new SudoShell(this, localShell, shell);
|
||||
|
||||
Logger.Debug($"A: {string.Join(", ", args)}");
|
||||
|
||||
#pragma warning disable CA2012
|
||||
return CalcCompletions(sudoShell, args);
|
||||
#pragma warning restore CA2012
|
||||
});
|
||||
|
||||
LoadConsoleCommands();
|
||||
@@ -73,6 +87,8 @@ namespace Robust.Server.Console
|
||||
NetManager.RegisterNetMessage<MsgConCmdAck>();
|
||||
|
||||
NetManager.RegisterNetMessage<MsgConCmdReg>(message => HandleRegistrationRequest(message.MsgChannel));
|
||||
NetManager.RegisterNetMessage<MsgConCompletion>(HandleConCompletions);
|
||||
NetManager.RegisterNetMessage<MsgConCompletionResp>();
|
||||
}
|
||||
|
||||
private void ExecuteInShell(IConsoleShell shell, string command)
|
||||
@@ -92,32 +108,29 @@ namespace Robust.Server.Console
|
||||
{
|
||||
args.RemoveAt(0);
|
||||
var cmdArgs = args.ToArray();
|
||||
if (shell.Player != null) // remote client
|
||||
if (!ShellCanExecute(shell, cmdName))
|
||||
{
|
||||
if (_groupController.CanCommand((IPlayerSession) shell.Player, cmdName)) // client has permission
|
||||
{
|
||||
AnyCommandExecuted?.Invoke(shell, cmdName, command, cmdArgs);
|
||||
conCmd.Execute(shell, command, cmdArgs);
|
||||
}
|
||||
else
|
||||
shell.WriteError($"Unknown command: '{cmdName}'");
|
||||
}
|
||||
else // system console
|
||||
{
|
||||
AnyCommandExecuted?.Invoke(shell, cmdName, command, cmdArgs);
|
||||
conCmd.Execute(shell, command, cmdArgs);
|
||||
shell.WriteError($"Unknown command: '{cmdName}'");
|
||||
return;
|
||||
}
|
||||
|
||||
AnyCommandExecuted?.Invoke(shell, cmdName, command, cmdArgs);
|
||||
conCmd.Execute(shell, command, cmdArgs);
|
||||
}
|
||||
else
|
||||
shell.WriteError($"Unknown command: '{cmdName}'");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogManager.GetSawmill(SawmillName).Error($"{FormatPlayerString(shell.Player)}: ExecuteError - {command}:\n{e}");
|
||||
LogManager.GetSawmill(SawmillName)
|
||||
.Error($"{FormatPlayerString(shell.Player)}: ExecuteError - {command}:\n{e}");
|
||||
shell.WriteError($"There was an error while executing the command: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
private bool ShellCanExecute(IConsoleShell shell, string cmdName)
|
||||
{
|
||||
return shell.Player == null || _groupController.CanCommand((IPlayerSession)shell.Player, cmdName);
|
||||
}
|
||||
|
||||
private void HandleRegistrationRequest(INetChannel senderConnection)
|
||||
{
|
||||
var netMgr = IoCManager.Resolve<IServerNetManager>();
|
||||
@@ -168,6 +181,46 @@ namespace Robust.Server.Console
|
||||
return session != null ? $"{session.Name}" : "[HOST]";
|
||||
}
|
||||
|
||||
private async void HandleConCompletions(MsgConCompletion message)
|
||||
{
|
||||
var session = _players.GetSessionByChannel(message.MsgChannel);
|
||||
var shell = new ConsoleShell(this, session);
|
||||
|
||||
var result = await CalcCompletions(shell, message.Args);
|
||||
|
||||
var msg = new MsgConCompletionResp
|
||||
{
|
||||
Result = result,
|
||||
Seq = message.Seq
|
||||
};
|
||||
|
||||
if (!message.MsgChannel.IsConnected)
|
||||
return;
|
||||
|
||||
NetManager.ServerSendMessage(msg, message.MsgChannel);
|
||||
}
|
||||
|
||||
private ValueTask<CompletionResult> CalcCompletions(IConsoleShell shell, string[] args)
|
||||
{
|
||||
// Logger.Debug(string.Join(", ", args));
|
||||
|
||||
if (args.Length <= 1)
|
||||
{
|
||||
// Typing out command name, handle this ourselves.
|
||||
return ValueTask.FromResult(CompletionResult.FromOptions(
|
||||
RegisteredCommands.Values.Select(c => new CompletionOption(c.Command, c.Description))));
|
||||
}
|
||||
|
||||
var cmdName = args[0];
|
||||
if (!AvailableCommands.TryGetValue(cmdName, out var cmd))
|
||||
return ValueTask.FromResult(CompletionResult.Empty);
|
||||
|
||||
if (!ShellCanExecute(shell, cmdName))
|
||||
return ValueTask.FromResult(CompletionResult.Empty);
|
||||
|
||||
return cmd.GetCompletionAsync(shell, args[1..], default);
|
||||
}
|
||||
|
||||
private sealed class SudoShell : IConsoleShell
|
||||
{
|
||||
private readonly ServerConsoleHost _host;
|
||||
|
||||
@@ -1153,6 +1153,31 @@ namespace Robust.Shared
|
||||
public static readonly CVarDef<int> AczManifestCompressLevel =
|
||||
CVarDef.Create("acz.manifest_compress_level", 14, CVar.SERVERONLY);
|
||||
|
||||
/*
|
||||
* CON
|
||||
*/
|
||||
|
||||
/// <summary>
|
||||
/// Add artificial delay (in seconds) to console completion fetching, even for local commands.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Intended for debugging the console completion system.
|
||||
/// </remarks>
|
||||
public static readonly CVarDef<float> ConCompletionDelay =
|
||||
CVarDef.Create("con.completion_delay", 0f, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The amount of completions to show in console completion drop downs.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ConCompletionCount =
|
||||
CVarDef.Create("con.completion_count", 15, CVar.CLIENTONLY);
|
||||
|
||||
/// <summary>
|
||||
/// The minimum margin of options to keep on either side of the completion cursor, when scrolling through.
|
||||
/// </summary>
|
||||
public static readonly CVarDef<int> ConCompletionMargin =
|
||||
CVarDef.Create("con.completion_margin", 3, CVar.CLIENTONLY);
|
||||
|
||||
/*
|
||||
* THREAD
|
||||
*/
|
||||
|
||||
@@ -1,31 +1,99 @@
|
||||
using System;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Console;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Configuration
|
||||
{
|
||||
[SuppressMessage("ReSharper", "StringLiteralTypo")]
|
||||
internal abstract class SharedCVarCommand : IConsoleCommand
|
||||
internal sealed class CVarCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "cvar";
|
||||
public string Description => "Gets or sets a CVar.";
|
||||
public string Description => Loc.GetString("cmd-cvar-desc");
|
||||
|
||||
public string Help => @"cvar <name> [value]
|
||||
If a value is passed, the value is parsed and stored as the new value of the CVar.
|
||||
If not, the current value of the CVar is displayed.
|
||||
Use 'cvar ?' to get a list of all registered CVars.";
|
||||
public string Help => Loc.GetString("cmd-cvar-help");
|
||||
|
||||
public abstract void Execute(IConsoleShell shell, string argStr, string[] args);
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length is < 1 or > 2)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-cvar-invalid-args"));
|
||||
return;
|
||||
}
|
||||
|
||||
protected static object ParseObject(Type type, string input)
|
||||
var configManager = IoCManager.Resolve<IConfigurationManager>();
|
||||
var name = args[0];
|
||||
|
||||
if (name == "?")
|
||||
{
|
||||
var cvars = configManager.GetRegisteredCVars().OrderBy(c => c);
|
||||
shell.WriteLine(string.Join("\n", cvars));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!configManager.IsCVarRegistered(name))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-cvar-not-registered", ("cvar", name)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (args.Length == 1)
|
||||
{
|
||||
// Read CVar
|
||||
var value = configManager.GetCVar<object>(name);
|
||||
shell.WriteLine(value.ToString()!);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Write CVar
|
||||
var value = args[1];
|
||||
var type = configManager.GetCVarType(name);
|
||||
try
|
||||
{
|
||||
var parsed = ParseObject(type, value);
|
||||
configManager.SetCVar(name, parsed);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-cvar-parse-error", ("type", type)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
var cfg = IoCManager.Resolve<IConfigurationManager>();
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var helpQuestion = Loc.GetString("cmd-cvar-compl-list");
|
||||
|
||||
return CompletionResult.FromHintOptions(
|
||||
cfg.GetRegisteredCVars()
|
||||
.Select(c => new CompletionOption(c))
|
||||
.Union(new[] { new CompletionOption("?", helpQuestion) })
|
||||
.OrderBy(c => c.Value),
|
||||
Loc.GetString("cmd-cvar-arg-name"));
|
||||
}
|
||||
|
||||
var cvar = args[0];
|
||||
if (!cfg.IsCVarRegistered(cvar))
|
||||
return CompletionResult.Empty;
|
||||
|
||||
var type = cfg.GetCVarType(cvar);
|
||||
return CompletionResult.FromHint($"<{type.Name}>");
|
||||
}
|
||||
|
||||
private static object ParseObject(Type type, string input)
|
||||
{
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
if(bool.TryParse(input, out var val))
|
||||
if (bool.TryParse(input, out var val))
|
||||
return val;
|
||||
|
||||
if (int.TryParse(input, out var intVal))
|
||||
if (Parse.TryInt32(input, out var intVal))
|
||||
{
|
||||
if (intVal == 0) return false;
|
||||
if (intVal == 1) return true;
|
||||
@@ -41,15 +109,15 @@ Use 'cvar ?' to get a list of all registered CVars.";
|
||||
|
||||
if (type == typeof(int))
|
||||
{
|
||||
return int.Parse(input, CultureInfo.InvariantCulture);
|
||||
return Parse.Int32(input);
|
||||
}
|
||||
|
||||
if (type == typeof(float))
|
||||
{
|
||||
return float.Parse(input, CultureInfo.InvariantCulture);
|
||||
return Parse.Float(input);
|
||||
}
|
||||
|
||||
throw new NotImplementedException();
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using JetBrains.Annotations;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Console.Commands
|
||||
@@ -13,9 +13,9 @@ namespace Robust.Shared.Console.Commands
|
||||
private static readonly Regex CommentRegex = new Regex(@"^\s*#");
|
||||
|
||||
public string Command => "exec";
|
||||
public string Description => "Executes a script file from the game's data directory.";
|
||||
public string Help => "Usage: exec <fileName>\n" +
|
||||
"Each line in the file is executed as a single command, unless it starts with a #";
|
||||
public string Description => Loc.GetString("cmd-exec-desc");
|
||||
public string Help => Loc.GetString("cmd-exec-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var res = IoCManager.Resolve<IResourceManager>();
|
||||
@@ -51,5 +51,20 @@ namespace Robust.Shared.Console.Commands
|
||||
shell.ConsoleHost.AppendCommand(line);
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var res = IoCManager.Resolve<IResourceManager>();
|
||||
|
||||
var hint = Loc.GetString("cmd-exec-arg-filename");
|
||||
var options = CompletionHelper.UserFilePath(args[0], res.UserData);
|
||||
|
||||
return CompletionResult.FromHintOptions(options, hint);
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
115
Robust.Shared/Console/Commands/GcCommands.cs
Normal file
115
Robust.Shared/Console/Commands/GcCommands.cs
Normal file
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Runtime;
|
||||
using Robust.Shared.Localization;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class GcCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gc";
|
||||
public string Description => Loc.GetString("cmd-gc-desc");
|
||||
public string Help => Loc.GetString("cmd-gc-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
GC.Collect();
|
||||
}
|
||||
else
|
||||
{
|
||||
if (Parse.TryInt32(args[0], out var generation))
|
||||
GC.Collect(generation);
|
||||
else
|
||||
shell.WriteError(Loc.GetString("cmd-gc-failed-parse"));
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-gc-arg-generation"));
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcFullCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gcf";
|
||||
public string Description => Loc.GetString("cmd-gcf-desc");
|
||||
public string Help => Loc.GetString("cmd-gcf-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
|
||||
GC.Collect(2, GCCollectionMode.Forced, true, true);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class GcModeCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "gc_mode";
|
||||
|
||||
public string Description => Loc.GetString("cmd-gc_mode-desc");
|
||||
|
||||
public string Help => Loc.GetString("cmd-gc_mode-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var prevMode = GCSettings.LatencyMode;
|
||||
if (args.Length == 0)
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-current", ("prevMode", prevMode.ToString())));
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-possible"));
|
||||
|
||||
foreach (var mode in Enum.GetValues<GCLatencyMode>())
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-option", ("mode", mode.ToString())));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse(args[0], true, out GCLatencyMode mode))
|
||||
{
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-unknown", ("arg", args[0])));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-attempt", ("prevMode", prevMode.ToString()),
|
||||
("mode", mode.ToString())));
|
||||
GCSettings.LatencyMode = mode;
|
||||
shell.WriteLine(Loc.GetString("cmd-gc_mode-result", ("mode", GCSettings.LatencyMode.ToString())));
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
return CompletionResult.FromHintOptions(
|
||||
Enum.GetNames<GCLatencyMode>(),
|
||||
Loc.GetString("cmd-gc_mode-arg-type"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class MemCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "mem";
|
||||
public string Description => Loc.GetString("cmd-mem-desc");
|
||||
public string Help => Loc.GetString("cmd-mem-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var info = GC.GetGCMemoryInfo();
|
||||
|
||||
var heapSize = info.HeapSizeBytes;
|
||||
var totalMemory = GC.GetTotalMemory(false);
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-mem-report", ("heapSize", heapSize), ("totalAllocated", totalMemory)));
|
||||
}
|
||||
}
|
||||
51
Robust.Shared/Console/Commands/HelpCommand.cs
Normal file
51
Robust.Shared/Console/Commands/HelpCommand.cs
Normal file
@@ -0,0 +1,51 @@
|
||||
using System.Linq;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class HelpCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "help";
|
||||
public string Description => Loc.GetString("cmd-help-desc");
|
||||
public string Help => Loc.GetString("cmd-help-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
switch (args.Length)
|
||||
{
|
||||
case 0:
|
||||
shell.WriteLine(Loc.GetString("cmd-help-no-args"));
|
||||
break;
|
||||
|
||||
case 1:
|
||||
var commandName = args[0];
|
||||
if (!shell.ConsoleHost.RegisteredCommands.TryGetValue(commandName, out var cmd))
|
||||
{
|
||||
shell.WriteError(Loc.GetString("cmd-help-unknown", ("command", commandName)));
|
||||
return;
|
||||
}
|
||||
|
||||
shell.WriteLine(Loc.GetString("cmd-help-top", ("command", cmd.Command),
|
||||
("description", cmd.Description)));
|
||||
shell.WriteLine(cmd.Help);
|
||||
break;
|
||||
|
||||
default:
|
||||
shell.WriteError(Loc.GetString("cmd-help-invalid-args"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
{
|
||||
var host = shell.ConsoleHost;
|
||||
return CompletionResult.FromHintOptions(
|
||||
host.RegisteredCommands.Values.OrderBy(c => c).Select(c => new CompletionOption(c.Command, c.Description)).ToArray(),
|
||||
Loc.GetString("cmd-help-arg-cmdname"));
|
||||
}
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
27
Robust.Shared/Console/Commands/ListAssembliesCommand.cs
Normal file
27
Robust.Shared/Console/Commands/ListAssembliesCommand.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Runtime.Loader;
|
||||
using System.Text;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class ListAssembliesCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "lsasm";
|
||||
public string Description => Loc.GetString("cmd-lsasm-desc");
|
||||
public string Help => Loc.GetString("cmd-lsasm-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (var context in AssemblyLoadContext.All)
|
||||
{
|
||||
sb.Append($"{context.Name}:\n");
|
||||
foreach (var assembly in context.Assemblies)
|
||||
{
|
||||
sb.Append($" {assembly.FullName}\n");
|
||||
}
|
||||
}
|
||||
|
||||
shell.WriteLine(sb.ToString());
|
||||
}
|
||||
}
|
||||
45
Robust.Shared/Console/Commands/ListCommand.cs
Normal file
45
Robust.Shared/Console/Commands/ListCommand.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using Robust.Shared.Localization;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class ListCommands : IConsoleCommand
|
||||
{
|
||||
public string Command => "list";
|
||||
|
||||
public string Description => Loc.GetString("cmd-list-desc");
|
||||
|
||||
public string Help => Loc.GetString("cmd-list-help");
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
var filter = "";
|
||||
if (args.Length == 1)
|
||||
filter = args[0];
|
||||
|
||||
var host = (IConsoleHostInternal)shell.ConsoleHost;
|
||||
|
||||
var builder = new StringBuilder(Loc.GetString("cmd-list-heading"));
|
||||
foreach (var command in host.RegisteredCommands.Values
|
||||
.Where(p => p.Command.Contains(filter))
|
||||
.OrderBy(c => c.Command))
|
||||
{
|
||||
//TODO: Make this actually check permissions.
|
||||
|
||||
var side = host.IsCmdServer(command) ? "S" : "C";
|
||||
builder.AppendLine($"{side} {command.Command,-32}{command.Description}");
|
||||
}
|
||||
|
||||
var message = builder.ToString().Trim(' ', '\n');
|
||||
shell.WriteLine(message);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
if (args.Length == 1)
|
||||
return CompletionResult.FromHint(Loc.GetString("cmd-list-arg-filter"));
|
||||
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
121
Robust.Shared/Console/Commands/LogCommands.cs
Normal file
121
Robust.Shared/Console/Commands/LogCommands.cs
Normal file
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Robust.Shared.IoC;
|
||||
using Robust.Shared.Log;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class LogSetLevelCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "loglevel";
|
||||
public string Description => "Changes the log level for a provided sawmill.";
|
||||
|
||||
public string Help => "Usage: loglevel <sawmill> <level>"
|
||||
+ "\n sawmill: A label prefixing log messages. This is the one you're setting the level for."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 2)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 2 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
LogLevel? level;
|
||||
if (levelname == "null")
|
||||
{
|
||||
level = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
|
||||
level = result;
|
||||
}
|
||||
|
||||
Logger.GetSawmill(name).Level = level;
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
var logMgr = IoCManager.Resolve<ILogManager>();
|
||||
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromHintOptions(
|
||||
logMgr.AllSawmills.Select(c => c.Name).OrderBy(c => c),
|
||||
"<sawmill>");
|
||||
case 2:
|
||||
return CompletionResult.FromHintOptions(
|
||||
Enum.GetNames<LogLevel>(),
|
||||
"<level>");
|
||||
|
||||
default:
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class TestLog : IConsoleCommand
|
||||
{
|
||||
public string Command => "testlog";
|
||||
public string Description => "Writes a test log to a sawmill.";
|
||||
|
||||
public string Help => "Usage: testlog <sawmill> <level> <message>"
|
||||
+ "\n sawmill: A label prefixing the logged message."
|
||||
+ "\n level: The log level. Must match one of the values of the LogLevel enum."
|
||||
+ "\n message: The message to be logged. Wrap this in double quotes if you want to use spaces.";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
if (args.Length != 3)
|
||||
{
|
||||
shell.WriteError("Invalid argument amount. Expected 3 arguments.");
|
||||
return;
|
||||
}
|
||||
|
||||
var name = args[0];
|
||||
var levelname = args[1];
|
||||
var message = args[2]; // yes this doesn't support spaces idgaf.
|
||||
if (!Enum.TryParse<LogLevel>(levelname, out var result))
|
||||
{
|
||||
shell.WriteLine("Failed to parse 2nd argument. Must be one of the values of the LogLevel enum.");
|
||||
return;
|
||||
}
|
||||
|
||||
var level = result;
|
||||
|
||||
Logger.LogS(level, name, message);
|
||||
}
|
||||
|
||||
public CompletionResult GetCompletion(IConsoleShell shell, string[] args)
|
||||
{
|
||||
var logMgr = IoCManager.Resolve<ILogManager>();
|
||||
|
||||
switch (args.Length)
|
||||
{
|
||||
case 1:
|
||||
return CompletionResult.FromHintOptions(
|
||||
logMgr.AllSawmills.Select(c => c.Name).OrderBy(c => c),
|
||||
"<sawmill>");
|
||||
case 2:
|
||||
return CompletionResult.FromHintOptions(
|
||||
Enum.GetNames<LogLevel>(),
|
||||
"<level>");
|
||||
|
||||
case 3:
|
||||
return CompletionResult.FromHint("<message>");
|
||||
|
||||
default:
|
||||
return CompletionResult.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
19
Robust.Shared/Console/Commands/SerializerStatsCommand.cs
Normal file
19
Robust.Shared/Console/Commands/SerializerStatsCommand.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using Robust.Shared.Serialization;
|
||||
|
||||
namespace Robust.Shared.Console.Commands;
|
||||
|
||||
internal sealed class SerializeStatsCommand : IConsoleCommand
|
||||
{
|
||||
public string Command => "szr_stats";
|
||||
public string Description => "Report serializer statistics.";
|
||||
public string Help => "szr_stats";
|
||||
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
shell.WriteLine($"serialized: {RobustSerializer.BytesSerialized} bytes, {RobustSerializer.ObjectsSerialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectSerializedBytes} bytes, {RobustSerializer.LargestObjectSerializedType} objects");
|
||||
shell.WriteLine($"deserialized: {RobustSerializer.BytesDeserialized} bytes, {RobustSerializer.ObjectsDeserialized} objects");
|
||||
shell.WriteLine($"largest serialized: {RobustSerializer.LargestObjectDeserializedBytes} bytes, {RobustSerializer.LargestObjectDeserializedType} objects");
|
||||
}
|
||||
}
|
||||
|
||||
66
Robust.Shared/Console/CompletionHelper.cs
Normal file
66
Robust.Shared/Console/CompletionHelper.cs
Normal file
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Robust.Shared.ContentPack;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Console;
|
||||
|
||||
/// <summary>
|
||||
/// Helpers for creating various completion results.
|
||||
/// </summary>
|
||||
public static class CompletionHelper
|
||||
{
|
||||
public static IEnumerable<CompletionOption> ContentFilePath(string arg, IResourceManager res)
|
||||
{
|
||||
var curPath = arg;
|
||||
if (curPath == "")
|
||||
curPath = "/";
|
||||
|
||||
var resPath = new ResourcePath(curPath);
|
||||
|
||||
if (!curPath.EndsWith("/"))
|
||||
resPath = (resPath / "..").Clean();
|
||||
|
||||
var options = res.ContentGetDirectoryEntries(resPath)
|
||||
.OrderBy(c => c)
|
||||
.Select(c =>
|
||||
{
|
||||
var opt = (resPath / c).ToString();
|
||||
|
||||
if (c.EndsWith("/"))
|
||||
return new CompletionOption(opt + "/", Flags: CompletionOptionFlags.PartialCompletion);
|
||||
|
||||
return new CompletionOption(opt);
|
||||
});
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
public static IEnumerable<CompletionOption> UserFilePath(string arg, IWritableDirProvider provider)
|
||||
{
|
||||
var curPath = arg;
|
||||
if (curPath == "")
|
||||
curPath = "/";
|
||||
|
||||
var resPath = new ResourcePath(curPath);
|
||||
|
||||
if (!resPath.IsRooted)
|
||||
return Enumerable.Empty<CompletionOption>();
|
||||
|
||||
if (!curPath.EndsWith("/"))
|
||||
resPath = (resPath / "..").Clean();
|
||||
|
||||
var entries = provider.DirectoryEntries(resPath);
|
||||
|
||||
return entries
|
||||
.Select(c =>
|
||||
{
|
||||
var full = resPath / c;
|
||||
if (provider.IsDir(full))
|
||||
return new CompletionOption($"{full}/", Flags: CompletionOptionFlags.PartialCompletion);
|
||||
|
||||
return new CompletionOption(full.ToString());
|
||||
})
|
||||
.OrderBy(c => c.Value);
|
||||
}
|
||||
}
|
||||
71
Robust.Shared/Console/CompletionResult.cs
Normal file
71
Robust.Shared/Console/CompletionResult.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Robust.Shared.Console;
|
||||
|
||||
/// <summary>
|
||||
/// Contains the result of a command completion.
|
||||
/// </summary>
|
||||
public sealed record CompletionResult(CompletionOption[] Options, string? Hint)
|
||||
{
|
||||
/// <summary>
|
||||
/// The possible full arguments to complete with. These are from the start of the entire argument.
|
||||
/// </summary>
|
||||
public CompletionOption[] Options { get; init; } = Options;
|
||||
|
||||
/// <summary>
|
||||
/// Type hint string for the current argument being typed.
|
||||
/// </summary>
|
||||
public string? Hint { get; init; } = Hint;
|
||||
|
||||
public static readonly CompletionResult Empty = new(Array.Empty<CompletionOption>(), null);
|
||||
|
||||
public static CompletionResult FromHintOptions(IEnumerable<string> options, string? hint) => new(ConvertOptions(options), hint);
|
||||
public static CompletionResult FromHintOptions(IEnumerable<CompletionOption> options, string? hint) => new(options.ToArray(), hint);
|
||||
|
||||
public static CompletionResult FromOptions(IEnumerable<string> options) => new(ConvertOptions(options), null);
|
||||
public static CompletionResult FromOptions(IEnumerable<CompletionOption> options) => new(options.ToArray(), null);
|
||||
|
||||
public static CompletionResult FromHint(string hint) => new(Array.Empty<CompletionOption>(), hint);
|
||||
|
||||
private static CompletionOption[] ConvertOptions(IEnumerable<string> stringOpts)
|
||||
{
|
||||
return stringOpts.Select(c => new CompletionOption(c)).ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Possible option to tab-complete in a <see cref="CompletionResult"/>.
|
||||
/// </summary>
|
||||
public record struct CompletionOption(string Value, string? Hint = null, CompletionOptionFlags Flags = default)
|
||||
{
|
||||
/// <summary>
|
||||
/// The value that will be filled in if completed.
|
||||
/// </summary>
|
||||
public string Value { get; set; } = Value;
|
||||
|
||||
/// <summary>
|
||||
/// Additional hint value that is shown to users, but not included in the completed value.
|
||||
/// </summary>
|
||||
public string? Hint { get; set; } = Hint;
|
||||
|
||||
/// <summary>
|
||||
/// Flags that control how this completion is used.
|
||||
/// </summary>
|
||||
public CompletionOptionFlags Flags { get; set; } = Flags;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flag options for <see cref="CompletionOption"/>.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum CompletionOptionFlags
|
||||
{
|
||||
/// <summary>
|
||||
/// The completion is "partial", it does complete the whole argument.
|
||||
/// Therefore, tab completing it should keep the cursor on the current argument
|
||||
/// (instead of adding a space to go to the next one).
|
||||
/// </summary>
|
||||
PartialCompletion = 1 << 0,
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
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;
|
||||
@@ -23,8 +25,7 @@ namespace Robust.Shared.Console
|
||||
[Dependency] private readonly IDynamicTypeFactoryInternal _typeFactory = default!;
|
||||
[Dependency] private readonly IGameTiming _timing = default!;
|
||||
|
||||
[ViewVariables]
|
||||
protected readonly Dictionary<string, IConsoleCommand> AvailableCommands = new();
|
||||
[ViewVariables] protected readonly Dictionary<string, IConsoleCommand> AvailableCommands = new();
|
||||
|
||||
private readonly CommandBuffer _commandBuffer = new CommandBuffer();
|
||||
|
||||
@@ -53,7 +54,7 @@ namespace Robust.Shared.Console
|
||||
// search for all client commands in all assemblies, and register them
|
||||
foreach (var type in ReflectionManager.GetAllChildren<IConsoleCommand>())
|
||||
{
|
||||
var instance = (IConsoleCommand) _typeFactory.CreateInstanceUnchecked(type, true);
|
||||
var instance = (IConsoleCommand)_typeFactory.CreateInstanceUnchecked(type, true);
|
||||
if (RegisteredCommands.TryGetValue(instance.Command, out var duplicate))
|
||||
{
|
||||
throw new InvalidImplementationException(instance.GetType(), typeof(IConsoleCommand),
|
||||
@@ -64,8 +65,11 @@ namespace Robust.Shared.Console
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RegisterCommand(string command, string description, string help, ConCommandCallback callback)
|
||||
public void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback)
|
||||
{
|
||||
if (AvailableCommands.ContainsKey(command))
|
||||
throw new InvalidOperationException($"Command already registered: {command}");
|
||||
@@ -74,6 +78,34 @@ namespace Robust.Shared.Console
|
||||
AvailableCommands.Add(command, newCmd);
|
||||
}
|
||||
|
||||
public void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionCallback completionCallback)
|
||||
{
|
||||
if (AvailableCommands.ContainsKey(command))
|
||||
throw new InvalidOperationException($"Command already registered: {command}");
|
||||
|
||||
var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback);
|
||||
AvailableCommands.Add(command, newCmd);
|
||||
}
|
||||
|
||||
public void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionAsyncCallback completionCallback)
|
||||
{
|
||||
if (AvailableCommands.ContainsKey(command))
|
||||
throw new InvalidOperationException($"Command already registered: {command}");
|
||||
|
||||
var newCmd = new RegisteredCommand(command, description, help, callback, completionCallback);
|
||||
AvailableCommands.Add(command, newCmd);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UnregisterCommand(string command)
|
||||
{
|
||||
@@ -81,7 +113,8 @@ namespace Robust.Shared.Console
|
||||
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.");
|
||||
throw new InvalidOperationException(
|
||||
"You cannot unregister commands that have been registered automatically.");
|
||||
|
||||
AvailableCommands.Remove(command);
|
||||
}
|
||||
@@ -157,6 +190,8 @@ namespace Robust.Shared.Console
|
||||
public sealed class RegisteredCommand : IConsoleCommand
|
||||
{
|
||||
public ConCommandCallback Callback { get; }
|
||||
public ConCommandCompletionCallback? CompletionCallback { get; }
|
||||
public ConCommandCompletionAsyncCallback? CompletionCallbackAsync { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Command { get; }
|
||||
@@ -174,7 +209,12 @@ namespace Robust.Shared.Console
|
||||
/// <param name="description">Short description of the command.</param>
|
||||
/// <param name="help">Extended description for the command.</param>
|
||||
/// <param name="callback">Callback function that is ran when the command is executed.</param>
|
||||
public RegisteredCommand(string command, string description, string help, ConCommandCallback callback)
|
||||
/// <param name="completionCallback">Callback function to get console completions.</param>
|
||||
public RegisteredCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback)
|
||||
{
|
||||
Command = command;
|
||||
// Should these two be localized somehow?
|
||||
@@ -183,11 +223,63 @@ namespace Robust.Shared.Console
|
||||
Callback = callback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="RegisteredCommand"/>.
|
||||
/// </summary>
|
||||
/// <param name="command">Name of the command.</param>
|
||||
/// <param name="description">Short description of the command.</param>
|
||||
/// <param name="help">Extended description for the command.</param>
|
||||
/// <param name="callback">Callback function that is ran when the command is executed.</param>
|
||||
/// <param name="completionCallback">Callback function to get console completions.</param>
|
||||
public RegisteredCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionCallback completionCallback) : this(command, description, help, callback)
|
||||
{
|
||||
CompletionCallback = completionCallback;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of <see cref="RegisteredCommand"/>.
|
||||
/// </summary>
|
||||
/// <param name="command">Name of the command.</param>
|
||||
/// <param name="description">Short description of the command.</param>
|
||||
/// <param name="help">Extended description for the command.</param>
|
||||
/// <param name="callback">Callback function that is ran when the command is executed.</param>
|
||||
/// <param name="completionCallback">Asynchronous callback function to get console completions.</param>
|
||||
public RegisteredCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionAsyncCallback completionCallback)
|
||||
: this(command, description, help, callback)
|
||||
{
|
||||
CompletionCallbackAsync = completionCallback;
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Execute(IConsoleShell shell, string argStr, string[] args)
|
||||
{
|
||||
Callback(shell, argStr, args);
|
||||
}
|
||||
|
||||
public ValueTask<CompletionResult> GetCompletionAsync(
|
||||
IConsoleShell shell,
|
||||
string[] args,
|
||||
CancellationToken cancel)
|
||||
{
|
||||
if (CompletionCallbackAsync != null)
|
||||
return CompletionCallbackAsync(shell, args);
|
||||
|
||||
if (CompletionCallback != null)
|
||||
return ValueTask.FromResult(CompletionCallback(shell, args));
|
||||
|
||||
return ValueTask.FromResult(CompletionResult.Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using JetBrains.Annotations;
|
||||
|
||||
namespace Robust.Shared.Console
|
||||
@@ -40,5 +42,40 @@ namespace Robust.Shared.Console
|
||||
/// <param name="argStr">Unparsed text of the complete command with arguments.</param>
|
||||
/// <param name="args">An array of all the parsed arguments.</param>
|
||||
void Execute(IConsoleShell shell, string argStr, string[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Fetches completion results for a typing a command.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Refrain from doing simple <c>.StartsWith(</c> filtering based on the currently typing command.
|
||||
/// The client already does this filtering on its own,
|
||||
/// so doing it manually would reduce responsiveness thanks to network lag.
|
||||
/// It may however be desirable to do larger-scale filtering.
|
||||
/// For example when typing out a resource path you could manually filter level-by-level as it's being typed.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Only arguments to the left of the cursor are passed.
|
||||
/// If the user puts their cursor in the middle of a line and starts typing, anything to the right is ignored.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="shell">The console that is typing this command.</param>
|
||||
/// <param name="args">The set of commands currently being typed.
|
||||
/// If the last parameter is an empty string, it basically represents that the user hit space after the previous term and should already get completion results,
|
||||
/// even if they haven't started typing the new argument yet.</param>
|
||||
/// <returns>The possible completion results presented to the user.</returns>
|
||||
/// <seealso cref="GetCompletionAsync"/>
|
||||
CompletionResult GetCompletion(IConsoleShell shell, string[] args) => CompletionResult.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Fetches completion results for typing a command, async variant. See <see cref="GetCompletion"/> for details.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If this method is implemented, <see cref="GetCompletion"/> will not be automatically called.
|
||||
/// </remarks>
|
||||
ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args, CancellationToken cancel)
|
||||
{
|
||||
return ValueTask.FromResult(GetCompletion(shell, args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Players;
|
||||
|
||||
namespace Robust.Shared.Console
|
||||
@@ -12,6 +13,16 @@ namespace Robust.Shared.Console
|
||||
/// <param name="args">An array of all the parsed arguments.</param>
|
||||
public delegate void ConCommandCallback(IConsoleShell shell, string argStr, string[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Called to fetch completions for a console command. See <see cref="IConsoleCommand.GetCompletion"/> for details.
|
||||
/// </summary>
|
||||
public delegate CompletionResult ConCommandCompletionCallback(IConsoleShell shell, string[] args);
|
||||
|
||||
/// <summary>
|
||||
/// Called to fetch completions for a console command (async). See <see cref="IConsoleCommand.GetCompletionAsync"/> for details.
|
||||
/// </summary>
|
||||
public delegate ValueTask<CompletionResult> ConCommandCompletionAsyncCallback(IConsoleShell shell, string[] args);
|
||||
|
||||
public delegate void ConAnyCommandCallback(IConsoleShell shell, string commandName, string argStr, string[] args);
|
||||
|
||||
/// <summary>
|
||||
@@ -59,11 +70,57 @@ namespace Robust.Shared.Console
|
||||
/// <param name="command">A string as identifier for this command.</param>
|
||||
/// <param name="description">Short one sentence description of the command.</param>
|
||||
/// <param name="help">Command format string.</param>
|
||||
/// <param name="callback"></param>
|
||||
void RegisterCommand(string command, string description, string help, ConCommandCallback callback);
|
||||
/// <param name="callback">
|
||||
/// Callback to invoke when this command is executed.
|
||||
/// </param>
|
||||
void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a console command that has been registered previously with <see cref="RegisterCommand"/>.
|
||||
/// Registers a console command into the console system. This is an alternative to
|
||||
/// creating an <see cref="IConsoleCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">A string as identifier for this command.</param>
|
||||
/// <param name="description">Short one sentence description of the command.</param>
|
||||
/// <param name="help">Command format string.</param>
|
||||
/// <param name="callback">
|
||||
/// Callback to invoke when this command is executed.
|
||||
/// </param>
|
||||
/// <param name="completionCallback">
|
||||
/// Callback to fetch completions with.
|
||||
/// </param>
|
||||
void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionCallback completionCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Registers a console command into the console system. This is an alternative to
|
||||
/// creating an <see cref="IConsoleCommand"/> class.
|
||||
/// </summary>
|
||||
/// <param name="command">A string as identifier for this command.</param>
|
||||
/// <param name="description">Short one sentence description of the command.</param>
|
||||
/// <param name="help">Command format string.</param>
|
||||
/// <param name="callback">
|
||||
/// Callback to invoke when this command is executed.
|
||||
/// </param>
|
||||
/// <param name="completionCallback">
|
||||
/// Callback to fetch completions with (async variant).
|
||||
/// </param>
|
||||
void RegisterCommand(
|
||||
string command,
|
||||
string description,
|
||||
string help,
|
||||
ConCommandCallback callback,
|
||||
ConCommandCompletionAsyncCallback completionCallback);
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a console command that has been registered previously with <see cref="RegisterCommand(string,string,string,Robust.Shared.Console.ConCommandCallback)"/>.
|
||||
/// If the specified command was registered automatically or isn't registered at all, the method will throw.
|
||||
/// </summary>
|
||||
/// <param name="command">The string identifier for the command.</param>
|
||||
@@ -152,4 +209,13 @@ namespace Robust.Shared.Console
|
||||
/// </summary>
|
||||
void ClearLocalConsole();
|
||||
}
|
||||
|
||||
internal interface IConsoleHostInternal : IConsoleHost
|
||||
{
|
||||
/// <summary>
|
||||
/// Is this command executed on the server?
|
||||
/// Always true when ran from server, true for server-proxy commands on the client.
|
||||
/// </summary>
|
||||
bool IsCmdServer(IConsoleCommand cmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Robust.Shared.Log;
|
||||
using Robust.Shared.Utility;
|
||||
@@ -81,6 +82,23 @@ namespace Robust.Shared.ContentPack
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<string> GetEntries(ResourcePath path)
|
||||
{
|
||||
var fullPath = GetPath(path);
|
||||
if (!Directory.Exists(fullPath))
|
||||
return Enumerable.Empty<string>();
|
||||
|
||||
return Directory.EnumerateFileSystemEntries(fullPath)
|
||||
.Select(c =>
|
||||
{
|
||||
var rel = Path.GetRelativePath(fullPath, c);
|
||||
if (Directory.Exists(c))
|
||||
return rel + "/";
|
||||
|
||||
return rel;
|
||||
});
|
||||
}
|
||||
|
||||
[Conditional("DEBUG")]
|
||||
private void CheckPathCasing(ResourcePath path)
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
@@ -36,5 +37,22 @@ namespace Robust.Shared.ContentPack
|
||||
/// </summary>
|
||||
/// <returns>Enumeration of all relative file paths.</returns>
|
||||
IEnumerable<string> GetRelativeFilePaths();
|
||||
|
||||
IEnumerable<string> GetEntries(ResourcePath path)
|
||||
{
|
||||
var countDirs = path == ResourcePath.Self ? 0 : path.EnumerateSegments().Count();
|
||||
|
||||
var options = FindFiles(path).Select(c =>
|
||||
{
|
||||
var segCount = c.EnumerateSegments().Count();
|
||||
var newPath = c.EnumerateSegments().Skip(countDirs).First();
|
||||
if (segCount > countDirs + 1)
|
||||
newPath += "/";
|
||||
|
||||
return newPath;
|
||||
}).Distinct();
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,6 +123,18 @@ namespace Robust.Shared.ContentPack
|
||||
/// <exception cref="ArgumentNullException">Thrown if <paramref name="path"/> is null.</exception>
|
||||
IEnumerable<ResourcePath> ContentFindFiles(string path);
|
||||
|
||||
/// <summary>
|
||||
/// Gets entries in a content directory.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is not a performant API; the VFS does not work natively with these kinds of directory entries.
|
||||
/// This is intended for development tools such as console command completions,
|
||||
/// not for general-purpose resource management.
|
||||
/// </remarks>
|
||||
/// <param name="path"></param>
|
||||
/// <returns>A sequence of entry names. If the entry name ends in a slash, it's a directory.</returns>
|
||||
IEnumerable<string> ContentGetDirectoryEntries(ResourcePath path);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a list of paths to all top-level content directories
|
||||
/// </summary>
|
||||
|
||||
@@ -46,6 +46,8 @@ namespace Robust.Shared.ContentPack
|
||||
(IEnumerable<ResourcePath> files, IEnumerable<ResourcePath> directories) Find(string pattern,
|
||||
bool recursive = true);
|
||||
|
||||
IEnumerable<string> DirectoryEntries(ResourcePath path);
|
||||
|
||||
/// <summary>
|
||||
/// Tests if a path is a directory.
|
||||
/// </summary>
|
||||
@@ -91,4 +93,4 @@ namespace Robust.Shared.ContentPack
|
||||
/// <returns></returns>
|
||||
void Rename(ResourcePath oldPath, ResourcePath newPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,36 @@ namespace Robust.Shared.ContentPack
|
||||
return ContentFindFiles(new ResourcePath(path));
|
||||
}
|
||||
|
||||
public IEnumerable<string> ContentGetDirectoryEntries(ResourcePath path)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(path, nameof(path));
|
||||
|
||||
if (!path.IsRooted)
|
||||
throw new ArgumentException("Path is not rooted", nameof(path));
|
||||
|
||||
var entries = new HashSet<string>();
|
||||
|
||||
_contentRootsLock.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var (prefix, root) in _contentRoots)
|
||||
{
|
||||
if (!path.TryRelativeTo(prefix, out var relative))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
entries.UnionWith(root.GetEntries(relative));
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_contentRootsLock.ExitReadLock();
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ResourcePath> ContentFindFiles(ResourcePath path)
|
||||
{
|
||||
|
||||
@@ -78,6 +78,14 @@ namespace Robust.Shared.ContentPack
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public IEnumerable<string> DirectoryEntries(ResourcePath path)
|
||||
{
|
||||
if (!TryGetNodeAt(path, out var dir) || dir is not DirectoryNode dirNode)
|
||||
throw new ArgumentException("Path is not a valid directory node.");
|
||||
|
||||
return dirNode.Children.Keys;
|
||||
}
|
||||
|
||||
public bool IsDir(ResourcePath path)
|
||||
{
|
||||
return TryGetNodeAt(path, out var node) && node is DirectoryNode;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.ContentPack
|
||||
@@ -76,6 +77,16 @@ namespace Robust.Shared.ContentPack
|
||||
return (resFiles, resDirs);
|
||||
}
|
||||
|
||||
public IEnumerable<string> DirectoryEntries(ResourcePath path)
|
||||
{
|
||||
var fullPath = GetFullPath(path);
|
||||
|
||||
foreach (var entry in Directory.EnumerateFileSystemEntries(fullPath))
|
||||
{
|
||||
yield return Path.GetRelativePath(fullPath, entry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsDir(ResourcePath path)
|
||||
{
|
||||
|
||||
@@ -70,6 +70,8 @@ namespace Robust.Shared.Input
|
||||
public static readonly BoundKeyFunction TextScrollToBottom = "TextScrollToBottom";
|
||||
public static readonly BoundKeyFunction TextDelete = "TextDelete";
|
||||
public static readonly BoundKeyFunction TextTabComplete = "TextTabComplete";
|
||||
public static readonly BoundKeyFunction TextCompleteNext = "TextCompleteNext";
|
||||
public static readonly BoundKeyFunction TextCompletePrev = "TextCompletePrev";
|
||||
}
|
||||
|
||||
[Serializable, NetSerializable]
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Robust.Shared.Log
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Shared.Log
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages logging sawmills.
|
||||
@@ -15,5 +17,10 @@
|
||||
/// Gets the sawmill with the specified name. Creates a new one if necessary.
|
||||
/// </summary>
|
||||
ISawmill GetSawmill(string name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of all currently created sawmills.
|
||||
/// </summary>
|
||||
IEnumerable<ISawmill> AllSawmills { get; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Robust.Shared.Utility;
|
||||
|
||||
namespace Robust.Shared.Log
|
||||
{
|
||||
@@ -41,6 +43,16 @@ namespace Robust.Shared.Log
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<ISawmill> AllSawmills
|
||||
{
|
||||
get
|
||||
{
|
||||
using var _ = _sawmillsLock.ReadGuard();
|
||||
|
||||
return sawmills.Values.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
private Sawmill _getSawmillUnlocked(string name)
|
||||
{
|
||||
if (sawmills.TryGetValue(name, out var sawmill))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Robust.Shared.Log
|
||||
{
|
||||
public sealed class ProxyLogManager : ILogManager
|
||||
@@ -15,5 +17,7 @@ namespace Robust.Shared.Log
|
||||
{
|
||||
return _impl.GetSawmill(name);
|
||||
}
|
||||
|
||||
public IEnumerable<ISawmill> AllSawmills => _impl.AllSawmills;
|
||||
}
|
||||
}
|
||||
|
||||
36
Robust.Shared/Network/Messages/MsgConCompletion.cs
Normal file
36
Robust.Shared/Network/Messages/MsgConCompletion.cs
Normal file
@@ -0,0 +1,36 @@
|
||||
using Lidgren.Network;
|
||||
|
||||
namespace Robust.Shared.Network.Messages;
|
||||
|
||||
#nullable disable
|
||||
|
||||
public sealed class MsgConCompletion : NetMessage
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.Command;
|
||||
|
||||
public int Seq { get; set; }
|
||||
public string[] Args { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||
{
|
||||
Seq = buffer.ReadInt32();
|
||||
|
||||
var len = buffer.ReadVariableInt32();
|
||||
Args = new string[len];
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
Args[i] = buffer.ReadString();
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||
{
|
||||
buffer.Write(Seq);
|
||||
|
||||
buffer.WriteVariableInt32(Args.Length);
|
||||
foreach (var arg in Args)
|
||||
{
|
||||
buffer.Write(arg);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
Robust.Shared/Network/Messages/MsgConCompletionResp.cs
Normal file
52
Robust.Shared/Network/Messages/MsgConCompletionResp.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using Lidgren.Network;
|
||||
using Robust.Shared.Console;
|
||||
|
||||
namespace Robust.Shared.Network.Messages;
|
||||
|
||||
#nullable disable
|
||||
|
||||
public sealed class MsgConCompletionResp : NetMessage
|
||||
{
|
||||
public override MsgGroups MsgGroup => MsgGroups.Command;
|
||||
|
||||
public int Seq { get; set; }
|
||||
public CompletionResult Result { get; set; }
|
||||
|
||||
public override void ReadFromBuffer(NetIncomingMessage buffer)
|
||||
{
|
||||
Seq = buffer.ReadInt32();
|
||||
|
||||
var len = buffer.ReadVariableInt32();
|
||||
var options = new CompletionOption[len];
|
||||
for (var i = 0; i < len; i++)
|
||||
{
|
||||
var optValue = buffer.ReadString();
|
||||
var optHint = buffer.ReadString();
|
||||
var optFlags = buffer.ReadInt32();
|
||||
|
||||
options[i] = new CompletionOption(
|
||||
optValue,
|
||||
optHint == "" ? null : optHint,
|
||||
(CompletionOptionFlags) optFlags);
|
||||
}
|
||||
|
||||
var hint = buffer.ReadString();
|
||||
|
||||
Result = new CompletionResult(options, hint == "" ? null : hint);
|
||||
}
|
||||
|
||||
public override void WriteToBuffer(NetOutgoingMessage buffer)
|
||||
{
|
||||
buffer.Write(Seq);
|
||||
|
||||
buffer.WriteVariableInt32(Result.Options.Length);
|
||||
foreach (var option in Result.Options)
|
||||
{
|
||||
buffer.Write(option.Value);
|
||||
buffer.Write(option.Hint);
|
||||
buffer.Write((int) option.Flags);
|
||||
}
|
||||
|
||||
buffer.Write(Result.Hint);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
#nullable enable
|
||||
using System.Text;
|
||||
using Robust.Shared.Utility.Collections;
|
||||
|
||||
namespace Robust.Shared.Utility
|
||||
{
|
||||
@@ -14,8 +14,18 @@ namespace Robust.Shared.Utility
|
||||
/// <param name="args">List of arguments to write into.</param>
|
||||
public static void ParseArguments(ReadOnlySpan<char> text, List<string> args)
|
||||
{
|
||||
var buf = "";
|
||||
var ranges = new ValueList<(int, int)>();
|
||||
ParseArguments(text, args, ref ranges);
|
||||
}
|
||||
|
||||
internal static void ParseArguments(
|
||||
ReadOnlySpan<char> text,
|
||||
List<string> args,
|
||||
ref ValueList<(int start, int end)> ranges)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var inQuotes = false;
|
||||
var startPos = -1;
|
||||
|
||||
for (var i = 0; i < text.Length; i++)
|
||||
{
|
||||
@@ -23,13 +33,14 @@ namespace Robust.Shared.Utility
|
||||
if (chr == '\\')
|
||||
{
|
||||
i += 1;
|
||||
startPos = i;
|
||||
if (i == text.Length)
|
||||
{
|
||||
buf += "\\";
|
||||
sb.Append('\\');
|
||||
break;
|
||||
}
|
||||
|
||||
buf += text[i];
|
||||
sb.Append(text[i]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -37,8 +48,15 @@ namespace Robust.Shared.Utility
|
||||
{
|
||||
if (inQuotes)
|
||||
{
|
||||
args.Add(buf);
|
||||
buf = "";
|
||||
args.Add(sb.ToString());
|
||||
sb.Clear();
|
||||
ranges.Add((startPos, i + 1));
|
||||
startPos = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (startPos < 0)
|
||||
startPos = i;
|
||||
}
|
||||
|
||||
inQuotes = !inQuotes;
|
||||
@@ -47,20 +65,27 @@ namespace Robust.Shared.Utility
|
||||
|
||||
if (chr == ' ' && !inQuotes)
|
||||
{
|
||||
if (buf != "")
|
||||
if (sb.Length != 0)
|
||||
{
|
||||
args.Add(buf);
|
||||
buf = "";
|
||||
args.Add(sb.ToString());
|
||||
sb.Clear();
|
||||
ranges.Add((startPos, i));
|
||||
startPos = -1;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
buf += chr;
|
||||
if (startPos < 0)
|
||||
startPos = i;
|
||||
|
||||
sb.Append(chr);
|
||||
}
|
||||
|
||||
if (buf != "")
|
||||
if (sb.Length != 0)
|
||||
{
|
||||
args.Add(buf);
|
||||
args.Add(sb.ToString());
|
||||
ranges.Add((startPos, text.Length));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user