using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Numerics;
using System.Text.RegularExpressions;
using Content.Server.Administration;
using Content.Shared.Administration;
using Robust.Shared.Console;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Content.Server._DV.Toolshed;
///
/// An extended version of the engine's do command. Runs a console command once per piped value,
/// substituting $NAME tokens in the command string first.
///
///
/// On top of the built-in tokens that do supports ($ID, $PID, $WX, $WY,
/// $LX, $LY, $SELF), any other $name is resolved as a toolshed variable and
/// converted to a string. Tokens that don't resolve to anything are left untouched.
///
/// Where the command actually runs is explicit:
///
/// - doext:client sends it back down to the calling user's own client to run locally. This is what
/// you want for client-side commands like exec, which resolve paths against the client's user data.
/// - doext:server runs it server-side as the calling session, like do does.
///
///
///
/// self doext:client "exec /Script_$var.txt"
/// i 5 => $count; self doext:server "somecommand $count"
///
[ToolshedCommand(Name = "doext"), AdminCommand(AdminFlags.Debug)]
public sealed class DoExtCommand : ToolshedCommand
{
///
/// Matches a $name token. Names use the same character set as toolshed variable names, so this
/// matches whole identifiers - $IDLE resolves as "IDLE" rather than being mangled into $ID.
///
private static readonly Regex TokenRegex = new(@"\$(\w+)", RegexOptions.Compiled);
[Dependency] private readonly IConsoleHost _console = default!;
private SharedTransformSystem? _xform;
///
/// Sends the command back down to the calling user's client, which runs it locally.
///
[CommandImplementation("client"), TakesPipedTypeAsGeneric]
public IEnumerable Client(IInvocationContext ctx, [PipedArgument] IEnumerable input, string command)
{
// No session means there's no client to hand this back to - e.g. the server console.
if (ctx.Session is not { } session)
{
ctx.ReportError(new NoClientSessionError());
yield break;
}
foreach (var value in input)
{
_console.RemoteExecuteCommand(session, Substitute(command, value, ctx));
yield return value;
}
}
///
/// Runs the command on the server as the calling session.
///
[CommandImplementation("server"), TakesPipedTypeAsGeneric]
public IEnumerable Server(IInvocationContext ctx, [PipedArgument] IEnumerable input, string command)
{
foreach (var value in input)
{
_console.ExecuteCommand(ctx.Session, Substitute(command, value, ctx));
yield return value;
}
}
private string Substitute(string command, T value, IInvocationContext ctx)
{
return TokenRegex.Replace(command, match =>
{
var name = match.Groups[1].Value;
if (TryGetBuiltin(name, value, ctx, out var builtin))
return builtin;
// Anything else is looked up as a toolshed variable and implicitly stringified.
if (ctx.ReadVar(name) is { } variable)
return Stringify(variable);
// Unresolved, so leave the token alone rather than silently blanking it.
return match.Value;
});
}
private bool TryGetBuiltin(string name, T value, IInvocationContext ctx, [NotNullWhen(true)] out string? result)
{
switch (name)
{
case "SELF":
result = Stringify(value);
return true;
case "PID":
result = (ctx.Session?.AttachedEntity ?? EntityUid.Invalid).ToString();
return true;
}
// The remaining tokens are all positional, so they only apply when piping entities.
if (value is not EntityUid uid)
{
result = null;
return false;
}
switch (name)
{
case "ID":
result = uid.ToString();
return true;
case "WX":
result = Number(WorldPosition(uid).X);
return true;
case "WY":
result = Number(WorldPosition(uid).Y);
return true;
case "LX":
result = Number(Transform(uid).Coordinates.X);
return true;
case "LY":
result = Number(Transform(uid).Coordinates.Y);
return true;
}
result = null;
return false;
}
private Vector2 WorldPosition(EntityUid uid)
{
_xform ??= GetSys();
return _xform.GetWorldPosition(uid);
}
private static string Number(float value) => value.ToString(CultureInfo.InvariantCulture);
private static string Stringify(object? value) => value?.ToString() ?? string.Empty;
}
public sealed class NoClientSessionError : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromUnformatted("There is no client to run this on. doext:client must be run by a player, not the server console.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}