* Saving work

* Move shit to engine

* lord

* merg

* awa

* bql is kill

* forgot the fucking bike rack

* bql is kill for real

* pjb will kill me

* aughfhbdj

* yo ho here we go on my way to the MINES

* a

* adgddf

* gdsgvfvxshngfgh

* b

* hfsjhghj

* hbfdjjh

* tf you mean i have to document it

* follow C# standards

* Assorted cleanup and documentation pass, minor bugfix in ValueRefParser.

* Start porting old commands, remove that pesky prefix in favor of integrating with the shell.

* Fix valueref up a bit, improve autocomplete for it.

* e

* Toolshed type system adventure.

* a log

* a

* a e i o u

* awa

* fix tests

* Arithmetic commands.

* a

* parse improvements

---------

Co-authored-by: moonheart08 <moonheart08@users.noreply.github.com>
This commit is contained in:
Moony
2023-08-02 16:06:16 -05:00
committed by GitHub
parent a7315b1c95
commit 0e1328675c
125 changed files with 6280 additions and 873 deletions

View File

@@ -39,7 +39,10 @@ END TEMPLATE-->
### New features
*None yet*
- Toolshed, a tacit shell language, has been introduced.
- Use Robust.Shared.ToolshedManager to invoke commands, with optional input and output.
- Implement IInvocationContext for custom invocation contexts i.e. scripting systems.
### Bugfixes

View File

@@ -17,15 +17,15 @@ cmd-error-dir-not-found = Could not find directory: {$dir}.
cmd-failure-no-attached-entity = There is no entity attached to this shell.
## 'help' command
cmd-help-desc = Display general help or help text for a specific command
cmd-help-help = Usage: help [command name]
cmd-oldhelp-desc = Display general help or help text for a specific command
cmd-oldhelp-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]
cmd-oldhelp-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-oldhelp-unknown = Unknown command: { $command }
cmd-oldhelp-top = { $command } - { $description }
cmd-oldhelp-invalid-args = Invalid amount of arguments.
cmd-oldhelp-arg-cmdname = [command name]
## 'cvar' command
cmd-cvar-desc = Gets or sets a CVar.

View File

@@ -0,0 +1,163 @@
command-description-tpto =
Teleport the given entities to some target entity.
command-description-player-list =
Returns a list of all player sessions.
command-description-player-self =
Returns the current player session.
command-description-player-imm =
Returns the session associated with the player given as argument.
command-description-player-entity =
Returns the entities of the input sessions.
command-description-self =
Returns the current attached entity.
command-description-physics-velocity =
Returns the velocity of the input entities.
command-description-physics-angular-velocity =
Returns the angular velocity of the input entities.
command-description-buildinfo =
Provides information about the build of the game.
command-description-cmd-list =
Returns a list of all commands, for this side.
command-description-explain =
Explains the given expression, providing command descriptions and signatures.
command-description-search =
Searches through the input for the provided value.
command-description-stopwatch =
Measures the execution time of the given expression.
command-description-types-consumers =
Provides all commands that can consume the given type.
command-description-types-tree =
Debug tool to return all types the command interpreter can downcast the input to.
command-description-types-gettype =
Returns the type of the input.
command-description-types-fullname =
Returns the full name of the input type according to CoreCLR.
command-description-as =
Casts the input to the given type.
Effectively a type hint if you know the type but the interpreter does not.
command-description-count =
Counts the amount of entries in it's input, returning an integer.
command-description-map =
Maps the input over the given block, with the provided expected return type.
This command may be modified to not need an explicit return type in the future.
command-description-select =
Selects N objects or N% of objects from the input.
One can additionally invert this command with not to make it select everything except N objects instead.
command-description-comp =
Returns the given component from the input entities, discarding entities without that component.
command-description-delete =
Deletes the input entities.
command-description-ent =
Returns the provided entity ID.
command-description-entities =
Returns all entities on the server.
command-description-paused =
Filters the input entities by whether or not they are paused.
This command can be inverted with not.
command-description-with =
Filters the input entities by whether or not they have the given component.
This command can be inverted with not.
command-description-fuck =
Throws an exception.
command-description-ecscomp-listty =
Lists every type of component registered.
command-description-cd =
Changes the session's current directory to the given relative or absolute path.
command-description-ls-here =
Lists the contents of the current directory.
command-description-ls-in =
Lists the contents of the given relative or absolute path.
command-description-methods-get =
Returns all methods associated with the input type.
command-description-methods-overrides =
Returns all methods overriden on the input type.
command-description-methods-overridesfrom =
Returns all methods overriden from the given type on the input type.
command-description-cmd-moo =
Asks the important questions.
command-description-cmd-descloc =
Returns the localization string for a command's description.
command-description-cmd-getshim =
Returns a command's execution shim.
command-description-help =
Provides a quick rundown of how to use toolshed.
command-description-ioc-registered =
Returns all the types registered with IoCManager on the current thread (usually the game thread)
command-description-ioc-get =
Gets an instance of an IoC registration.
command-description-loc-tryloc =
Tries to get a localization string, returning null if unable.
command-description-loc-loc =
Gets a localization string, returning the unlocalized string if unable.
command-description-physics-angular_velocity =
Returns the angular velocity of the given entities.
command-description-vars =
Provides a list of all variables set in this session.
command-description-any =
Returns true if there's any values in the input, otherwise false.
command-description-ArrowCommand =
Assigns the input to a variable.
command-description-isempty =
Returns true if the input is empty, otherwise false.
command-description-isnull =
Returns true if the input is null, otherwise false.
command-description-unique =
Filters the input sequence for uniqueness, removing duplicate values.
command-description-where =
Given some input sequence IEnumerable<T>, takes a block of signature T -> bool that decides if each input value should be included in the output sequence.
command-description-do =
Backwards compatibility with BQL, applies the given old commands over the input sequence.
command-description-named =
Filters the input entities by their name, with the regex $selector^.
command-description-prototyped =
Filters the input entities by their prototype.
command-description-nearby =
Creates a new list of all entities nearby the inputs within the given range.
command-description-first =
Returns the first entry of the given enumerable.
command-description-splat =
"Splats" a block, value, or variable, creating N copies of it in a list.
command-description-val =
Casts the given value, block, or variable to the given type. This is mostly a workaround for current limitations of variables.
command-description-actor-controlled =
Filters entities by whether or not they're actively controlled.
command-description-actor-session =
Returns the sessions associated with the input entities.
command-description-physics-parent =
Returns the parent(s) of the input entities.
command-description-emplace =
Runs the given block over it's inputs, with the input value placed into the variable $value within the block.
Additionally breaks out $wx, $wy, $proto, $desc, $name, and $paused for entities.
Can also have breakout values for other types, consult the documentation for that type for further info.
command-description-AddCommand =
Performs numeric addition.
command-description-SubtractCommand =
Performs numeric subtraction.
command-description-MultiplyCommand =
Performs numeric multiplication.
command-description-DivideCommand =
Performs numeric division.
command-description-min =
Returns the minimum of two values.
command-description-max =
Returns the maximum of two values.
command-description-BitAndCommand =
Performs bitwise AND.
command-description-BitOrCommand =
Performs bitwise OR.
command-description-BitXorCommand =
Performs bitwise XOR.
command-description-neg =
Negates the input.
command-description-GreaterThanCommand =
Performs a greater-than comparison, x > y.
command-description-LessThanCommand =
Performs a less-than comparison, x < y.
command-description-GreaterThanOrEqualCommand =
Performs a greater-than-or-equal comparison, x >= y.
command-description-LessThanOrEqualCommand =
Performs a less-than-or-equal comparison, x <= y.
command-description-EqualCommand =
Performs an equality comparison, returning true if the inputs are equal.
command-description-NotEqualCommand =
Performs an equality comparison, returning true if the inputs are not equal.

View File

@@ -14,7 +14,7 @@ internal sealed partial class ClientConsoleHost
private int _completionSeq;
public async Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel)
public async Task<CompletionResult> GetCompletions(List<string> args, string argStr, CancellationToken cancel)
{
// Last element is the command currently being typed. May be empty.
@@ -24,10 +24,10 @@ internal sealed partial class ClientConsoleHost
if (delay > 0)
await Task.Delay((int)(delay * 1000), cancel);
return await CalcCompletions(args, cancel);
return await CalcCompletions(args, argStr, cancel);
}
private Task<CompletionResult> CalcCompletions(List<string> args, CancellationToken cancel)
private Task<CompletionResult> CalcCompletions(List<string> args, string argStr, CancellationToken cancel)
{
if (args.Count == 1)
{
@@ -44,10 +44,10 @@ internal sealed partial class ClientConsoleHost
if (!AvailableCommands.TryGetValue(args[0], out var cmd))
return Task.FromResult(CompletionResult.Empty);
return cmd.GetCompletionAsync(LocalShell, args.ToArray()[1..], cancel).AsTask();
return cmd.GetCompletionAsync(LocalShell, args.ToArray()[1..], argStr, cancel).AsTask();
}
private Task<CompletionResult> DoServerCompletions(List<string> args, CancellationToken cancel)
private Task<CompletionResult> DoServerCompletions(List<string> args, string argStr, CancellationToken cancel)
{
var tcs = new TaskCompletionSource<CompletionResult>();
var cts = CancellationTokenSource.CreateLinkedTokenSource(cancel);
@@ -62,6 +62,7 @@ internal sealed partial class ClientConsoleHost
var msg = new MsgConCompletion
{
Args = args.ToArray(),
ArgString = argStr,
Seq = seq
};

View File

@@ -10,6 +10,7 @@ using Robust.Shared.Console;
using Robust.Shared.Enums;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
@@ -21,13 +22,13 @@ namespace Robust.Client.Console
{
public sealed class AddStringArgs : EventArgs
{
public string Text { get; }
public FormattedMessage Text { get; }
public bool Local { get; }
public bool Error { get; }
public AddStringArgs(string text, bool local, bool error)
public AddStringArgs(FormattedMessage text, bool local, bool error)
{
Text = text;
Local = local;
@@ -132,10 +133,17 @@ namespace Robust.Client.Console
AddFormatted?.Invoke(this, new AddFormattedMessageArgs(message));
}
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
{
AddFormattedLine(msg);
}
/// <inheritdoc />
public override void WriteError(ICommonSession? session, string text)
{
OutputText(text, true, true);
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(msg, true, true);
}
public bool IsCmdServer(IConsoleCommand cmd)
@@ -151,8 +159,13 @@ namespace Robust.Client.Console
if (string.IsNullOrWhiteSpace(command))
return;
WriteLine(null, "");
var msg = new FormattedMessage();
msg.PushColor(Color.Gold);
msg.AddText("> " + command);
msg.Pop();
// echo the command locally
WriteLine(null, "> " + command);
OutputText(msg, true, false);
//Commands are processed locally and then sent to the server to be processed there again.
var args = new List<string>();
@@ -205,7 +218,9 @@ namespace Robust.Client.Console
/// <inheritdoc />
public override void WriteLine(ICommonSession? session, string text)
{
OutputText(text, true, false);
var msg = new FormattedMessage();
msg.AddText(text);
OutputText(msg, true, false);
}
/// <inheritdoc />
@@ -214,12 +229,12 @@ namespace Robust.Client.Console
// We don't have anything to dispose.
}
private void OutputText(string text, bool local, bool error)
private void OutputText(FormattedMessage text, bool local, bool error)
{
AddString?.Invoke(this, new AddStringArgs(text, local, error));
var level = error ? LogLevel.Warning : LogLevel.Info;
_conLogger.Log(level, text);
_conLogger.Log(level, text.ToString());
}
private void OnNetworkConnected(object? sender, NetChannelArgs netChannelArgs)
@@ -229,7 +244,7 @@ namespace Robust.Client.Console
private void HandleConCmdAck(MsgConCmdAck msg)
{
OutputText("< " + msg.Text, false, msg.Error);
OutputText(msg.Text, false, msg.Error);
}
private void HandleConCmdReg(MsgConCmdReg msg)
@@ -303,13 +318,14 @@ namespace Robust.Client.Console
public async ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
string argStr,
CancellationToken cancel)
{
var host = (ClientConsoleHost)shell.ConsoleHost;
var argsList = args.ToList();
argsList.Insert(0, Command);
return await host.DoServerCompletions(argsList, cancel);
return await host.DoServerCompletions(argsList, argStr, cancel);
}
}
@@ -327,10 +343,11 @@ namespace Robust.Client.Console
public override async ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
string argStr,
CancellationToken cancel)
{
var host = (ClientConsoleHost)shell.ConsoleHost;
return await host.DoServerCompletions(args.ToList(), cancel);
return await host.DoServerCompletions(args.ToList(), argStr[">".Length..], cancel);
}
}
}

View File

@@ -19,6 +19,6 @@ namespace Robust.Client.Console
void AddFormattedLine(FormattedMessage message);
Task<CompletionResult> GetCompletions(List<string> args, CancellationToken cancel);
Task<CompletionResult> GetCompletions(List<string> args, string argStr, CancellationToken cancel);
}
}

View File

@@ -1,7 +1,7 @@
<Control xmlns="https://spacestation14.io"
xmlns:gfx="clr-namespace:Robust.Client.Graphics">
<BoxContainer Orientation="Vertical">
<OutputPanel Name="Output" VerticalExpand="True">
<OutputPanel Name="Output" VerticalExpand="True" StyleClasses="monospace">
<OutputPanel.StyleBoxOverride>
<gfx:StyleBoxFlat BackgroundColor="#25252add"
ContentMarginLeftOverride="3" ContentMarginRightOverride="3"

View File

@@ -90,7 +90,7 @@ public sealed partial class DebugConsole
private async void TypeUpdateCompletions(bool fullUpdate)
{
var (args, _, _) = CalcTypingArgs();
var (args, _, _, str) = CalcTypingArgs();
if (args.Count != _compParamCount)
{
@@ -101,7 +101,7 @@ public sealed partial class DebugConsole
if (fullUpdate)
{
var seq = ++_compSeqSend;
var task = _consoleHost.GetCompletions(args, _compCancel.Token);
var task = _consoleHost.GetCompletions(args, str, _compCancel.Token);
if (!task.IsCompleted)
{
@@ -140,7 +140,7 @@ public sealed partial class DebugConsole
if (_compCurResult == null)
return;
var (_, curTyping, _) = CalcTypingArgs();
var (_, curTyping, _, _) = CalcTypingArgs();
var curSelected = _compFiltered?.Length > 0 ? _compFiltered[_compSelected] : default;
_compFiltered = FilterCompletions(_compCurResult.Options, curTyping);
@@ -168,7 +168,7 @@ public sealed partial class DebugConsole
DebugTools.AssertNotNull(_compCurResult);
var (_, _, endRange) = CalcTypingArgs();
var (_, _, endRange, _) = CalcTypingArgs();
var offset = CommandBar.GetOffsetAtIndex(endRange.start);
// Logger.Debug($"Offset: {offset}");
@@ -231,7 +231,7 @@ public sealed partial class DebugConsole
}
}
private (List<string> args, string curTyping, (int start, int end) lastRange) CalcTypingArgs()
private (List<string> args, string curTyping, (int start, int end) lastRange, string argStr) CalcTypingArgs()
{
var cursor = CommandBar.CursorPosition;
// Don't consider text after the cursor.
@@ -252,7 +252,7 @@ public sealed partial class DebugConsole
else
lastRange = (cursor, cursor);
return (args, args[^1], lastRange);
return (args, args[^1], lastRange, text.ToString());
}
private CompletionOption[] FilterCompletions(IEnumerable<CompletionOption> completions, string curTyping)
@@ -270,7 +270,7 @@ public sealed partial class DebugConsole
{
// Figure out typing word so we know how much to replace.
var (completion, _, completionFlags) = _compFiltered[_compSelected];
var (_, _, lastRange) = CalcTypingArgs();
var (_, _, lastRange, _) = CalcTypingArgs();
// Replace the full word from the start.
// This means that letter casing will match the completion suggestion.

View File

@@ -23,9 +23,9 @@ namespace Robust.Client.UserInterface.CustomControls
/// <summary>
/// Write a line with a specific color to the console window.
/// </summary>
void AddLine(string text, Color color);
void AddLine(FormattedMessage text, Color color);
void AddLine(string text);
void AddLine(FormattedMessage text);
void AddFormattedLine(FormattedMessage message);
@@ -108,7 +108,7 @@ namespace Robust.Client.UserInterface.CustomControls
private Color DetermineColor(bool local, bool error)
{
return Color.White;
return error ? Color.Red : Color.White;
}
protected override void FrameUpdate(FrameEventArgs args)
@@ -136,16 +136,16 @@ namespace Robust.Client.UserInterface.CustomControls
_flushHistoryToDisk();
}
public void AddLine(string text, Color color)
public void AddLine(FormattedMessage text, Color color)
{
var formatted = new FormattedMessage(3);
formatted.PushColor(color);
formatted.AddText(text);
formatted.AddMessage(text);
formatted.Pop();
AddFormattedLine(formatted);
}
public void AddLine(string text)
public void AddLine(FormattedMessage text)
{
AddLine(text, Color.White);
}

View File

@@ -15,6 +15,9 @@ public sealed class DefaultStylesheet
{
var notoSansFont = res.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSans-Regular.ttf");
var notoSansFont12 = new VectorFont(notoSansFont, 12);
var notoSansMonoFont = res.GetResource<FontResource>("/EngineFonts/NotoSans/NotoSansMono-Regular.ttf");
var notoSansMono12 = new VectorFont(notoSansMonoFont, 12);
var theme = userInterfaceManager.CurrentTheme;
@@ -38,6 +41,13 @@ public sealed class DefaultStylesheet
Stylesheet = new Stylesheet(new StyleRule[]
{
/*
* Debug console and other monospace things.
*/
Element().Class("monospace")
.Prop("font", notoSansMono12),
/*
* OS Window defaults
*/

View File

@@ -32,6 +32,7 @@ using Robust.Shared.Profiling;
using Robust.Shared.Prototypes;
using Robust.Shared.Reflection;
using Robust.Shared.Replays;
using Robust.Shared.Toolshed;
using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
@@ -371,6 +372,7 @@ namespace Robust.Server
_prototype.ResolveResults();
_refMan.Initialize();
IoCManager.Resolve<ToolshedManager>().Initialize();
_consoleHost.Initialize();
_entityManager.Startup();
_mapManager.Startup();

View File

@@ -1,162 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Pidgin;
using Robust.Shared.GameObjects;
using static Pidgin.Parser;
using static Pidgin.Parser<char>;
namespace Robust.Server.Bql
{
public partial class BqlQueryManager
{
private readonly Dictionary<Type, Parser<char, BqlQuerySelectorParsed>> _parsers = new();
private Parser<char, BqlQuerySelectorParsed> _allQuerySelectors = default!;
private Parser<char, (IEnumerable<BqlQuerySelectorParsed>, string)> SimpleQuery => Parser.Map((en, _, rest) => (en, rest), SkipWhitespaces.Then(_allQuerySelectors).Many(), String("do").Then(SkipWhitespaces), Any.ManyString());
private static Parser<char, string> Word => OneOf(LetterOrDigit, Char('_')).ManyString();
private static Parser<char, object> Objectify<T>(Parser<char, T> inp)
{
return Parser.Map(x => (object) x!, inp);
}
private struct SubstitutionData
{
public string Name;
public SubstitutionData(string name)
{
Name = name;
}
}
private static Parser<char, SubstitutionData> Substitution =>
Try(Char('$').Then(OneOf(Uppercase, Char('_')).ManyString()))
.MapWithInput((x, _) => new SubstitutionData(x.ToString()));
private static Parser<char, int> Integer =>
Try(Int(10));
private static Parser<char, object> SubstitutableInteger =>
Objectify(Integer).Or(Objectify(Try(Substitution)));
private static Parser<char, double> Float =>
Try(Real);
private static Parser<char, object> SubstitutableFloat =>
Objectify(Float).Or(Objectify(Try(Substitution)));
private static Parser<char, double> Percentage =>
Try(Real).Before(Char('%'));
private static Parser<char, object> SubstitutablePercentage =>
Objectify(Percentage).Or(Objectify(Try(Substitution)));
private static Parser<char, EntityUid> EntityId =>
Try(Parser.Map(x => new EntityUid(x), Int(10)));
private static Parser<char, object> SubstitutableEntityId =>
Objectify(EntityId).Or(Objectify(Try(Substitution)));
private Parser<char, Type> Component =>
Try(Parser.Map(t => _componentFactory.GetRegistration(t).Type, Word));
private Parser<char, object> SubstitutableComponent =>
Objectify(Component).Or(Objectify(Try(Substitution)));
private static Parser<char, string> QuotedString =>
OneOf(Try(Char('"').Then(OneOf(new []
{
AnyCharExcept("\"")
}).ManyString().Before(Char('"')))), Try(Word));
private static Parser<char, object> SubstitutableString =>
Objectify(QuotedString).Or(Objectify(Try(Substitution)));
// thing to make sure it all compiles.
[UsedImplicitly]
private Parser<char, object> TypeSystemCheck =>
OneOf(new[]
{
Objectify(Integer),
Objectify(Percentage),
Objectify(EntityId),
Objectify(Component),
Objectify(Float),
Objectify(QuotedString)
});
private Parser<char, BqlQuerySelectorParsed> BuildBqlQueryParser(BqlQuerySelector inst)
{
if (inst.Arguments.Length == 0)
{
return Parser.Map(_ => new BqlQuerySelectorParsed(new List<object>(), inst.Token, false), SkipWhitespaces);
}
List<Parser<char, object>> argsParsers = new();
foreach (var (arg, _) in inst.Arguments.Select((x, i) => (x, i)))
{
List<Parser<char, object>> choices = new();
if ((arg & QuerySelectorArgument.String) == QuerySelectorArgument.String)
{
choices.Add(Try(SubstitutableString.Before(SkipWhitespaces).Labelled("string argument")));
}
if ((arg & QuerySelectorArgument.Component) == QuerySelectorArgument.Component)
{
choices.Add(Try(SubstitutableComponent.Before(SkipWhitespaces).Labelled("component argument")));
}
if ((arg & QuerySelectorArgument.EntityId) == QuerySelectorArgument.EntityId)
{
choices.Add(Try(SubstitutableEntityId.Before(SkipWhitespaces).Labelled("entity ID argument")));
}
if ((arg & QuerySelectorArgument.Integer) == QuerySelectorArgument.Integer)
{
choices.Add(Try(SubstitutableInteger.Before(SkipWhitespaces).Labelled("integer argument")));
}
if ((arg & QuerySelectorArgument.Percentage) == QuerySelectorArgument.Percentage)
{
choices.Add(Try(SubstitutablePercentage.Before(SkipWhitespaces).Labelled("percentage argument")));
}
if ((arg & QuerySelectorArgument.Float) == QuerySelectorArgument.Float)
{
choices.Add(Try(SubstitutableFloat.Before(SkipWhitespaces).Labelled("float argument")));
}
argsParsers.Add(OneOf(choices));
}
Parser<char, List<object>> finalParser = argsParsers[0].Map(x => new List<object> { x });
for (var i = 1; i < argsParsers.Count; i++)
{
finalParser = finalParser.Then(argsParsers[i], (list, o) =>
{
list.Add(o);
return list;
}).Labelled("arguments");
}
return Parser.Map(args => new BqlQuerySelectorParsed(args, inst.Token, false), finalParser);
}
private void DoParserSetup()
{
foreach (var inst in _instances)
{
_parsers.Add(inst.GetType(), BuildBqlQueryParser(inst));
}
_allQuerySelectors = Parser.Map((a,b) => (a,b), Try(String("not").Before(Char(' '))).Optional(), OneOf(_instances.Select(x =>
Try(String(x.Token).Before(Char(' '))))).Then(tok =>
_parsers[_queriesByToken[tok].GetType()])
).Map(pair =>
{
pair.b.Inverted = pair.a.HasValue;
return pair.b;
});
}
}
}

View File

@@ -1,39 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Pidgin;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Server.Bql
{
public partial class BqlQueryManager
{
public (IEnumerable<EntityUid>, string) SimpleParseAndExecute(string query)
{
var parsed = SimpleQuery.Parse(query);
if (parsed.Success)
{
var selectors = parsed.Value.Item1.ToArray();
if (selectors.Length == 0)
{
return (_entityManager.GetEntities(), parsed.Value.Item2);
}
var entities = _queriesByToken[selectors[0].Token]
.DoInitialSelection(selectors[0].Arguments, selectors[0].Inverted, _entityManager);
foreach (var sel in selectors[1..])
{
entities = _queriesByToken[sel.Token].DoSelection(entities, sel.Arguments, sel.Inverted, _entityManager);
}
return (entities, parsed.Value.Item2);
}
else
{
throw new Exception(parsed.Error!.RenderErrorMessage());
}
}
}
}

View File

@@ -1,44 +0,0 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Reflection;
using Robust.Shared.Utility;
namespace Robust.Server.Bql
{
public sealed partial class BqlQueryManager : IBqlQueryManager
{
[Dependency] private readonly IReflectionManager _reflectionManager = default!;
[Dependency] private readonly IComponentFactory _componentFactory = default!;
[Dependency] private readonly IEntityManager _entityManager = default!;
private readonly List<BqlQuerySelector> _instances = new();
private readonly Dictionary<string, BqlQuerySelector> _queriesByToken = new();
/// <summary>
/// Automatically registers all query selectors with the parser/executor.
/// </summary>
public void DoAutoRegistrations()
{
foreach (var type in _reflectionManager.FindTypesWithAttribute<RegisterBqlQuerySelectorAttribute>())
{
RegisterClass(type);
}
DoParserSetup();
}
/// <summary>
/// Internally registers the given <see cref="BqlQuerySelector"/>.
/// </summary>
/// <param name="bqlQuerySelector">The selector to register</param>
private void RegisterClass(Type bqlQuerySelector)
{
DebugTools.Assert(bqlQuerySelector.BaseType == typeof(BqlQuerySelector));
var inst = (BqlQuerySelector)Activator.CreateInstance(bqlQuerySelector)!;
_instances.Add(inst);
_queriesByToken.Add(inst.Token, inst);
}
}
}

View File

@@ -1,368 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Prototypes;
namespace Robust.Server.Bql
{
[RegisterBqlQuerySelector]
public sealed class WithQuerySelector : BqlQuerySelector
{
public override string Token => "with";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Component };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var comp = (Type) arguments[0];
return input.Where(x => entityManager.HasComponent(x, comp) ^ isInverted);
}
public override IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
if (isInverted)
{
return base.DoInitialSelection(arguments, isInverted, entityManager);
}
return entityManager.GetAllComponents((Type) arguments[0], includePaused: true)
.Select(x => x.Uid);
}
}
[RegisterBqlQuerySelector]
public sealed class NamedQuerySelector : BqlQuerySelector
{
public override string Token => "named";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var r = new Regex("^" + (string) arguments[0] + "$");
return input.Where(e =>
{
if (entityManager.TryGetComponent<MetaDataComponent>(e, out var metaDataComponent))
return r.IsMatch(metaDataComponent.EntityName) ^ isInverted;
return isInverted;
});
}
}
[RegisterBqlQuerySelector]
public sealed class ParentedToQuerySelector : BqlQuerySelector
{
public override string Token => "parentedto";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.EntityId };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var uid = (EntityUid) arguments[0];
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) &&
transform.ParentUid == uid) ^ isInverted);
}
}
[RegisterBqlQuerySelector]
public sealed class RecursiveParentedToQuerySelector : BqlQuerySelector
{
public override string Token => "rparentedto";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.EntityId };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var uid = (EntityUid) arguments[0];
return input.Where(e =>
{
if (!entityManager.TryGetComponent<TransformComponent>(e, out var transform))
return isInverted;
while (transform.ParentUid != EntityUid.Invalid)
{
if ((transform.ParentUid == uid) ^ isInverted)
return true;
if (!entityManager.TryGetComponent(transform.ParentUid, out transform))
return false;
}
return false;
});
}
}
[RegisterBqlQuerySelector]
public sealed class ChildrenQuerySelector : BqlQuerySelector
{
public override string Token => "children";
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
foreach (var uid in input)
{
if (!entityManager.TryGetComponent(uid, out TransformComponent? xform)) continue;
foreach (var child in xform.ChildEntities)
{
yield return child;
}
}
}
}
[RegisterBqlQuerySelector]
public sealed class RecursiveChildrenQuerySelector : BqlQuerySelector
{
public override string Token => "rchildren";
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments,
bool isInverted, IEntityManager entityManager)
{
IEnumerable<EntityUid> toSearch = input;
while (true)
{
// TODO: Reduce LINQ chaining
var doing = toSearch.Where(entityManager.HasComponent<TransformComponent>).Select(entityManager.GetComponent<TransformComponent>).ToArray();
var search = doing.SelectMany(x => x.ChildEntities);
if (!search.Any())
break;
toSearch = doing.SelectMany(x => x.ChildEntities).Where(x => x != EntityUid.Invalid);
foreach (var xform in doing)
{
foreach (var uid in xform.ChildEntities)
{
// This should never happen anyway
if (uid == EntityUid.Invalid) continue;
yield return uid;
}
}
}
}
}
[RegisterBqlQuerySelector]
public sealed class ParentQuerySelector : BqlQuerySelector
{
public override string Token => "parent";
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
return input.Where(entityManager.HasComponent<TransformComponent>)
.Distinct();
}
public override IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
return DoSelection(
Query(entityManager.AllEntityQueryEnumerator<TransformComponent>()),
arguments,
isInverted, entityManager);
IEnumerable<EntityUid> Query(AllEntityQueryEnumerator<TransformComponent> enumerator)
{
while (enumerator.MoveNext(out var entityUid, out _))
{
yield return entityUid;
}
}
}
}
[RegisterBqlQuerySelector]
public sealed class AboveQuerySelector : BqlQuerySelector
{
public override string Token => "above";
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.String };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var tileDefinitionManager = IoCManager.Resolve<ITileDefinitionManager>();
var tileTy = tileDefinitionManager[(string) arguments[0]];
var map = IoCManager.Resolve<IMapManager>();
if (tileTy.TileId == 0)
{
return input.Where(e => entityManager.TryGetComponent<TransformComponent>(e, out var transform) && (transform.GridUid is null) ^ isInverted);
}
else
{
return input.Where(e =>
{
if (!entityManager.TryGetComponent<TransformComponent>(e, out var transform)) return isInverted;
if (!map.TryGetGrid(transform.GridUid, out var grid))
return isInverted;
return (grid.GetTileRef(transform.Coordinates).Tile.TypeId == tileTy.TileId) ^ isInverted;
});
}
}
}
[RegisterBqlQuerySelector]
// ReSharper disable once InconsistentNaming the name is correct shut up
public sealed class OnGridQuerySelector : BqlQuerySelector
{
public override string Token => "ongrid";
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.Integer };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
// TODO: Probably easier and significantly faster to just iterate the grid's children.
var grid = new EntityUid((int) arguments[0]);
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.GridUid == grid) ^ isInverted);
}
}
[RegisterBqlQuerySelector]
// ReSharper disable once InconsistentNaming the name is correct shut up
public sealed class OnMapQuerySelector : BqlQuerySelector
{
public override string Token => "onmap";
public override QuerySelectorArgument[] Arguments => new [] { QuerySelectorArgument.Integer };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
// TODO: Just use EntityLookup GetEntitiesInMap
var map = new MapId((int) arguments[0]);
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.MapID == map) ^ isInverted);
}
}
[RegisterBqlQuerySelector]
public sealed class PrototypedQuerySelector : BqlQuerySelector
{
public override string Token => "prototyped";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var name = (string) arguments[0];
return input.Where(e => (entityManager.TryGetComponent<MetaDataComponent>(e, out var metaData) && metaData.EntityPrototype?.ID == name) ^ isInverted);
}
}
[RegisterBqlQuerySelector]
public sealed class RecursivePrototypedQuerySelector : BqlQuerySelector
{
public override string Token => "rprototyped";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.String };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var name = (string) arguments[0];
return input.Where(e =>
{
if (!entityManager.TryGetComponent<MetaDataComponent>(e, out var metaData))
return isInverted;
if ((metaData.EntityPrototype?.ID == name) ^ isInverted)
return true;
var prototypeManager = IoCManager.Resolve<IPrototypeManager>();
return metaData.EntityPrototype != null && prototypeManager.EnumerateParents<EntityPrototype>(metaData.EntityPrototype.ID).Any(x => x.Name == name) ^ isInverted;
});
}
}
[RegisterBqlQuerySelector]
public sealed class SelectQuerySelector : BqlQuerySelector
{
public override string Token => "select";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Integer | QuerySelectorArgument.Percentage };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
if (arguments[0] is int)
{
var inp = input.OrderBy(_ => Guid.NewGuid()).ToArray();
var taken = (int) arguments[0];
if (isInverted)
taken = Math.Max(0, inp.Length - taken);
return inp.Take(taken);
}
var enumerable = input.OrderBy(_ => Guid.NewGuid()).ToArray();
var amount = isInverted
? (int) Math.Floor(enumerable.Length * Math.Clamp(1 - (double) arguments[0], 0, 1))
: (int) Math.Floor(enumerable.Length * Math.Clamp((double) arguments[0], 0, 1));
return enumerable.Take(amount);
}
}
[RegisterBqlQuerySelector]
public sealed class NearQuerySelector : BqlQuerySelector
{
public override string Token => "near";
public override QuerySelectorArgument[] Arguments => new []{ QuerySelectorArgument.Float };
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
var radius = (float)(double)arguments[0];
var entityLookup = entityManager.System<EntityLookupSystem>();
var xformQuery = entityManager.GetEntityQuery<TransformComponent>();
var distinct = new HashSet<EntityUid>();
foreach (var uid in input)
{
foreach (var near in entityLookup.GetEntitiesInRange(xformQuery.GetComponent(uid).Coordinates,
radius))
{
if (!distinct.Add(near)) continue;
yield return near;
}
}
//BUG: GetEntitiesInRange effectively uses manhattan distance. This is not intended, near is supposed to be circular.
}
}
[RegisterBqlQuerySelector]
// ReSharper disable once InconsistentNaming the name is correct shut up
public sealed class AnchoredQuerySelector : BqlQuerySelector
{
public override string Token => "anchored";
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
return input.Where(e => (entityManager.TryGetComponent<TransformComponent>(e, out var transform) && transform.Anchored) ^ isInverted);
}
}
[RegisterBqlQuerySelector]
public sealed class PausedQuerySelector : BqlQuerySelector
{
public override string Token => "paused";
public override QuerySelectorArgument[] Arguments => Array.Empty<QuerySelectorArgument>();
public override IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input, IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
return input.Where(e => entityManager.GetComponent<MetaDataComponent>(e).EntityPaused ^ isInverted);
}
}
}

View File

@@ -1,63 +0,0 @@
using System;
using System.Collections.Generic;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Server.Bql
{
[Flags]
[PublicAPI]
public enum QuerySelectorArgument
{
Integer = 0b00000001,
Float = 0b00000010,
String = 0b00000100,
Percentage = 0b00001000,
Component = 0b00010000,
//SubQuery = 0b00100000,
EntityId = 0b01000000,
}
[PublicAPI]
public abstract class BqlQuerySelector
{
/// <summary>
/// The token name for the given QuerySelector, for example `when`.
/// </summary>
public virtual string Token => throw new NotImplementedException();
/// <summary>
/// Arguments for the given QuerySelector, presented as "what arguments are permitted in what spot".
/// </summary>
public virtual QuerySelectorArgument[] Arguments => throw new NotImplementedException();
/// <summary>
/// Performs a transform over it's input entity list, whether that be filtering (selecting) or expanding the
/// input on some criteria like what entities are nearby.
/// </summary>
/// <param name="input">Input entity list.</param>
/// <param name="arguments">Parsed selector arguments.</param>
/// <param name="isInverted">Whether the query is inverted.</param>
/// <param name="entityManager">The entity manager.</param>
/// <returns>New list of entities</returns>
/// <exception cref="NotImplementedException">someone is a moron if this happens.</exception>
public abstract IEnumerable<EntityUid> DoSelection(IEnumerable<EntityUid> input,
IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager);
/// <summary>
/// Performs selection as the first selector in the query. Allows for optimizing when you can be more efficient
/// than just querying every entity.
/// </summary>
/// <param name="arguments"></param>
/// <param name="isInverted"></param>
/// <param name="entityManager"></param>
/// <returns></returns>
public virtual IEnumerable<EntityUid> DoInitialSelection(IReadOnlyList<object> arguments, bool isInverted, IEntityManager entityManager)
{
return DoSelection(entityManager.GetEntities(), arguments, isInverted, entityManager);
}
[UsedImplicitly]
protected BqlQuerySelector() {}
}
}

View File

@@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
namespace Robust.Server.Bql
{
public struct BqlQuerySelectorParsed
{
public List<object> Arguments;
public string Token;
public bool Inverted;
public BqlQuerySelectorParsed(List<object> arguments, string token, bool inverted)
{
Arguments = arguments;
Token = token;
Inverted = inverted;
}
}
}

View File

@@ -1,78 +0,0 @@
using System.Globalization;
using System.Linq;
using Robust.Server.Player;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Server.Bql
{
public sealed class ForAllCommand : LocalizedCommands
{
[Dependency] private readonly IBqlQueryManager _bql = default!;
[Dependency] private readonly IEntityManager _entities = default!;
public override string Command => "forall";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
if (args.Length < 2)
{
shell.WriteLine(Help);
return;
}
var transformSystem = _entities.System<SharedTransformSystem>();
var (entities, rest) = _bql.SimpleParseAndExecute(argStr[6..]);
foreach (var ent in entities.ToList())
{
var cmds = SubstituteEntityDetails(_entities, transformSystem, shell, ent, rest).Split(";");
foreach (var cmd in cmds)
{
shell.ExecuteCommand(cmd);
}
}
}
// This will be refactored out soon.
private static string SubstituteEntityDetails(
IEntityManager entMan,
SharedTransformSystem transformSystem,
IConsoleShell shell,
EntityUid ent,
string ruleString)
{
var transform = entMan.GetComponent<TransformComponent>(ent);
var metadata = entMan.GetComponent<MetaDataComponent>(ent);
var worldPos = transformSystem.GetWorldPosition(transform);
var localPos = transform.LocalPosition;
// gross, is there a better way to do this?
ruleString = ruleString.Replace("$ID", ent.ToString());
ruleString = ruleString.Replace("$WX", worldPos.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$WY", worldPos.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$LX", localPos.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$LY", localPos.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$NAME", metadata.EntityName);
if (shell.Player is { AttachedEntity: { } pEntity})
{
var pTransform = entMan.GetComponent<TransformComponent>(pEntity);
var pWorldPos = transformSystem.GetWorldPosition(pTransform);
var pLocalPos = pTransform.LocalPosition;
ruleString = ruleString.Replace("$PID", pEntity.ToString());
ruleString = ruleString.Replace("$PWX", pWorldPos.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PWY", pWorldPos.Y.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PLX", pLocalPos.X.ToString(CultureInfo.InvariantCulture));
ruleString = ruleString.Replace("$PLY", pLocalPos.Y.ToString(CultureInfo.InvariantCulture));
}
return ruleString;
}
}
}

View File

@@ -1,11 +0,0 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
namespace Robust.Server.Bql
{
public interface IBqlQueryManager
{
public (IEnumerable<EntityUid>, string) SimpleParseAndExecute(string query);
void DoAutoRegistrations();
}
}

View File

@@ -1,14 +0,0 @@
using System;
using JetBrains.Annotations;
namespace Robust.Server.Bql
{
[AttributeUsage(AttributeTargets.Class, Inherited = false)]
[BaseTypeRequired(typeof(BqlQuerySelector))]
[MeansImplicitUse]
[PublicAPI]
public sealed class RegisterBqlQuerySelectorAttribute : Attribute
{
}
}

View File

@@ -1,8 +1,12 @@
using Robust.Server.Player;
using Robust.Shared.Map;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
namespace Robust.Server.Console
{
internal sealed class ConGroupController : IConGroupController
internal sealed class ConGroupController : IConGroupController, IPermissionController
{
public IConGroupControllerImplementation? Implementation { get; set; }
@@ -30,5 +34,11 @@ namespace Robust.Server.Console
{
return Implementation?.CanAdminReloadPrototypes(session) ?? false;
}
public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error)
{
error = null;
return Implementation?.CheckInvokable(command, user, out error) ?? false;
}
}
}

View File

@@ -1,8 +1,9 @@
using Robust.Server.Player;
using Robust.Shared.Toolshed;
namespace Robust.Server.Console
{
public interface IConGroupControllerImplementation
public interface IConGroupControllerImplementation : IPermissionController
{
bool CanCommand(IPlayerSession session, string cmdName);
bool CanAdminPlace(IPlayerSession session);

View File

@@ -9,6 +9,8 @@ using Robust.Shared.Log;
using Robust.Shared.Network;
using Robust.Shared.Network.Messages;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;
namespace Robust.Server.Console
@@ -19,6 +21,7 @@ namespace Robust.Server.Console
[Dependency] private readonly IConGroupController _groupController = default!;
[Dependency] private readonly IPlayerManager _players = default!;
[Dependency] private readonly ISystemConsoleManager _systemConsole = default!;
[Dependency] private readonly ToolshedManager _toolshed = default!;
public ServerConsoleHost() : base(isServer: true) {}
@@ -45,19 +48,31 @@ namespace Robust.Server.Console
/// <inheritdoc />
public override void WriteLine(ICommonSession? session, string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
if (session is IPlayerSession playerSession)
OutputText(playerSession, text, false);
OutputText(playerSession, msg, false);
else
OutputText(null, text, false);
OutputText(null, msg, false);
}
public override void WriteLine(ICommonSession? session, FormattedMessage msg)
{
if (session is IPlayerSession playerSession)
OutputText(playerSession, msg, false);
else
OutputText(null, msg, false);
}
/// <inheritdoc />
public override void WriteError(ICommonSession? session, string text)
{
var msg = new FormattedMessage();
msg.AddText(text);
if (session is IPlayerSession playerSession)
OutputText(playerSession, text, true);
OutputText(playerSession, msg, true);
else
OutputText(null, text, true);
OutputText(null, msg, true);
}
public bool IsCmdServer(IConsoleCommand cmd) => true;
@@ -70,13 +85,13 @@ namespace Robust.Server.Console
var localShell = shell.ConsoleHost.LocalShell;
var sudoShell = new SudoShell(this, localShell, shell);
ExecuteInShell(sudoShell, argStr["sudo ".Length..]);
}, (shell, args) =>
}, (shell, args, argStr) =>
{
var localShell = shell.ConsoleHost.LocalShell;
var sudoShell = new SudoShell(this, localShell, shell);
#pragma warning disable CA2012
return CalcCompletions(sudoShell, args);
return CalcCompletions(sudoShell, args, argStr);
#pragma warning restore CA2012
});
@@ -117,6 +132,18 @@ namespace Robust.Server.Console
AnyCommandExecuted?.Invoke(shell, cmdName, command, cmdArgs);
conCmd.Execute(shell, command, cmdArgs);
}
else
{
// toolshed time
_toolshed.InvokeCommand(shell, command, null, out var res, out var ctx);
foreach (var err in ctx.GetErrors())
{
ctx.WriteLine(err.Describe());
}
shell.WriteLine(_toolshed.PrettyPrintType(res));
}
}
catch (Exception e)
{
@@ -162,7 +189,7 @@ namespace Robust.Server.Console
ExecuteCommand(session, text);
}
private void OutputText(IPlayerSession? session, string text, bool error)
private void OutputText(IPlayerSession? session, FormattedMessage text, bool error)
{
if (session != null)
{
@@ -183,10 +210,25 @@ namespace Robust.Server.Console
private async void HandleConCompletions(MsgConCompletion message)
{
var session = _players.GetSessionByChannel(message.MsgChannel);
var shell = new ConsoleShell(this, session, false);
var result = await CalcCompletions(shell, message.Args);
var result = await CalcCompletions(shell, message.Args, message.ArgString);
if ((result.Options.Length == 0 && result.Hint is null) || message.Args.Length <= 1)
{
var parser = new ForwardParser(message.ArgString, _toolshed);
CommandRun.TryParse(false, true, parser, null, null, false, out _, out var completions, out _);
if (completions == null)
{
goto done;
}
var (shedRes, _) = await completions.Value;
shedRes ??= CompletionResult.Empty;
result = new CompletionResult(shedRes.Options.Concat(result.Options).ToArray(), shedRes.Hint ?? result.Hint);
}
done:
var msg = new MsgConCompletionResp
{
Result = result,
@@ -199,7 +241,7 @@ namespace Robust.Server.Console
NetManager.ServerSendMessage(msg, message.MsgChannel);
}
private ValueTask<CompletionResult> CalcCompletions(IConsoleShell shell, string[] args)
private ValueTask<CompletionResult> CalcCompletions(IConsoleShell shell, string[] args, string argStr)
{
// Logger.Debug(string.Join(", ", args));
@@ -217,7 +259,7 @@ namespace Robust.Server.Console
if (!ShellCanExecute(shell, cmdName))
return ValueTask.FromResult(CompletionResult.Empty);
return cmd.GetCompletionAsync(shell, args[1..], default);
return cmd.GetCompletionAsync(shell, args[1..], argStr, default);
}
private sealed class SudoShell : IConsoleShell
@@ -254,6 +296,12 @@ namespace Robust.Server.Console
_sudoer.WriteLine(text);
}
public void WriteLine(FormattedMessage message)
{
_owner.WriteLine(message);
_sudoer.WriteLine(message);
}
public void WriteError(string text)
{
_owner.WriteError(text);

View File

@@ -1,4 +1,3 @@
using Robust.Server.Bql;
using Robust.Server.Configuration;
using Robust.Server.Console;
using Robust.Server.DataMetrics;
@@ -82,7 +81,6 @@ namespace Robust.Server
deps.Register<IScriptHost, ScriptHost>();
deps.Register<IMetricsManager, MetricsManager>();
deps.Register<IAuthManager, AuthManager>();
deps.Register<IBqlQueryManager, BqlQueryManager>();
deps.Register<HubManager, HubManager>();
deps.Register<IRobustSerializer, ServerRobustSerializer>();
deps.Register<IRobustSerializerInternal, ServerRobustSerializer>();

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Server.GameObjects;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed;
namespace Robust.Server.Toolshed.Commands.Players;
[ToolshedCommand]
public sealed class ActorCommand : ToolshedCommand
{
[CommandImplementation("controlled")]
public IEnumerable<EntityUid> Controlled([PipedArgument] IEnumerable<EntityUid> input)
{
return input.Where(HasComp<ActorComponent>);
}
[CommandImplementation("session")]
public IEnumerable<IPlayerSession> Session([PipedArgument] IEnumerable<EntityUid> input)
{
return input.Where(HasComp<ActorComponent>).Select(x => Comp<ActorComponent>(x).PlayerSession);
}
}

View File

@@ -0,0 +1,84 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Robust.Server.Player;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Server.Toolshed.Commands.Players;
[ToolshedCommand]
public sealed class PlayerCommand : ToolshedCommand
{
[Dependency] private readonly IPlayerManager _playerManager = default!;
[CommandImplementation("list")]
public IEnumerable<IPlayerSession> Players()
=> _playerManager.ServerSessions;
[CommandImplementation("self")]
public IPlayerSession Self([CommandInvocationContext] IInvocationContext ctx)
{
if (ctx.Session is null)
{
ctx.ReportError(new NotForServerConsoleError());
}
return (IPlayerSession)ctx.Session!;
}
[CommandImplementation("imm")]
public ICommonSession Immediate(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] string username
)
{
_playerManager.TryGetSessionByUsername(username, out var session);
if (session is null)
{
if (Guid.TryParse(username, out var guid))
{
_playerManager.TryGetSessionById(new NetUserId(guid), out session);
}
}
if (session is null)
{
ctx.ReportError(new NoSuchPlayerError(username));
}
return session!;
}
[CommandImplementation("entity")]
public IEnumerable<EntityUid> GetPlayerEntity([PipedArgument] IEnumerable<IPlayerSession> sessions)
{
return sessions.Select(x => x.AttachedEntity).Where(x => x is not null).Cast<EntityUid>();
}
[CommandImplementation("entity")]
public EntityUid GetPlayerEntity([PipedArgument] IPlayerSession sessions)
{
return sessions.AttachedEntity ?? default;
}
}
public record struct NoSuchPlayerError(string Username) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup($"No player with the username/GUID {Username} could be found.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,35 @@
using System.Reflection;
using System.Reflection.Emit;
using ILReader;
using ILReader.Readers;
using Robust.Shared.Toolshed;
namespace Robust.Shared.Scripting;
[ToolshedCommand]
public sealed class ILCommand : ToolshedCommand
{
[CommandImplementation("dumpil")]
public void DumpIL(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] MethodInfo info
)
{
var reader = GetReader(info);
foreach (var instruction in reader)
{
if (instruction is null)
break;
ctx.WriteLine(instruction.ToString()!);
}
}
private IILReader GetReader(MethodBase method)
{
IILReaderConfiguration cfg = ILReader.Configuration.Resolve(method);
return cfg.GetReader(method);
}
}

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ILReader.Core" Version="1.0.0.4" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Features" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Scripting" Version="4.0.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="4.0.1" />

View File

@@ -10,7 +10,10 @@ using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Map;
using Robust.Shared.Map.Components;
using Robust.Shared.Players;
using Robust.Shared.Prototypes;
using Robust.Shared.Toolshed;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Scripting
@@ -19,7 +22,7 @@ namespace Robust.Shared.Scripting
[SuppressMessage("ReSharper", "IdentifierTypo")]
[SuppressMessage("ReSharper", "InconsistentNaming")]
[SuppressMessage("ReSharper", "CA1822")]
public abstract class ScriptGlobalsShared
public abstract class ScriptGlobalsShared : IInvocationContext
{
private const BindingFlags DefaultHelpFlags =
BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public;
@@ -30,6 +33,8 @@ namespace Robust.Shared.Scripting
[field: Dependency] public IMapManager map { get; } = default!;
[field: Dependency] public IDependencyCollection dependencies { get; } = default!;
[field: Dependency] public ToolshedManager shed { get; } = default!;
protected ScriptGlobalsShared(IDependencyCollection dependencies)
{
dependencies.InjectDependencies(this);
@@ -161,6 +166,24 @@ namespace Robust.Shared.Scripting
public abstract void write(object toString);
public abstract void show(object obj);
public object? tsh(string toolshedCommand)
{
shed.InvokeCommand(this, toolshedCommand, null, out var res);
return res;
}
public T tsh<T>(string toolshedCommand)
{
shed.InvokeCommand(this, toolshedCommand, null, out var res);
return (T)res!;
}
public TOut tsh<TIn, TOut>(TIn value, string toolshedCommand)
{
shed.InvokeCommand(this, toolshedCommand, value, out var res);
return (TOut)res!;
}
#region EntityManager proxy methods
public T Comp<T>(EntityUid uid) where T : Component
=> ent.GetComponent<T>(uid);
@@ -227,5 +250,34 @@ namespace Robust.Shared.Scripting
return ent.EntityQuery<TComp1, TComp2, TComp3>(includePaused);
}
#endregion
public bool CheckInvokable(CommandSpec command, out IConError? error)
{
error = null;
return true; // Do as I say!
}
public ICommonSession? Session => null;
public void WriteLine(string line)
{
write(line);
}
public void ReportError(IConError err)
{
write(err);
}
public IEnumerable<IConError> GetErrors()
{
return Array.Empty<IConError>();
}
public void ClearErrors()
{
}
public Dictionary<string, object?> Variables { get; } = new();
}
}

View File

@@ -5,31 +5,31 @@ namespace Robust.Shared.Console.Commands;
internal sealed class HelpCommand : LocalizedCommands
{
public override string Command => "help";
public override string Command => "oldhelp";
public override void Execute(IConsoleShell shell, string argStr, string[] args)
{
switch (args.Length)
{
case 0:
shell.WriteLine(Loc.GetString("cmd-help-no-args"));
shell.WriteLine(Loc.GetString("cmd-oldhelp-no-args"));
break;
case 1:
var commandName = args[0];
if (!shell.ConsoleHost.AvailableCommands.TryGetValue(commandName, out var cmd))
{
shell.WriteError(Loc.GetString("cmd-help-unknown", ("command", commandName)));
shell.WriteError(Loc.GetString("cmd-oldhelp-unknown", ("command", commandName)));
return;
}
shell.WriteLine(Loc.GetString("cmd-help-top", ("command", cmd.Command),
shell.WriteLine(Loc.GetString("cmd-oldhelp-top", ("command", cmd.Command),
("description", cmd.Description)));
shell.WriteLine(cmd.Help);
break;
default:
shell.WriteError(Loc.GetString("cmd-help-invalid-args"));
shell.WriteError(Loc.GetString("cmd-oldhelp-invalid-args"));
break;
}
}
@@ -41,7 +41,7 @@ internal sealed class HelpCommand : LocalizedCommands
var host = shell.ConsoleHost;
return CompletionResult.FromHintOptions(
host.AvailableCommands.Values.OrderBy(c => c.Command).Select(c => new CompletionOption(c.Command, c.Description)).ToArray(),
Loc.GetString("cmd-help-arg-cmdname"));
Loc.GetString("cmd-oldhelp-arg-cmdname"));
}
return CompletionResult.Empty;

View File

@@ -11,6 +11,7 @@ using Robust.Shared.Network;
using Robust.Shared.Players;
using Robust.Shared.Reflection;
using Robust.Shared.Timing;
using Robust.Shared.Utility;
using Robust.Shared.ViewVariables;
namespace Robust.Shared.Console
@@ -173,6 +174,8 @@ namespace Robust.Shared.Console
//TODO: IConsoleOutput for [e#1225]
public abstract void WriteLine(ICommonSession? session, string text);
public abstract void WriteLine(ICommonSession? session, FormattedMessage msg);
public abstract void WriteError(ICommonSession? session, string text);
/// <inheritdoc />
@@ -324,10 +327,11 @@ namespace Robust.Shared.Console
public ValueTask<CompletionResult> GetCompletionAsync(
IConsoleShell shell,
string[] args,
string argStr,
CancellationToken cancel)
{
if (CompletionCallbackAsync != null)
return CompletionCallbackAsync(shell, args);
return CompletionCallbackAsync(shell, args, argStr);
if (CompletionCallback != null)
return ValueTask.FromResult(CompletionCallback(shell, args));

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Shared.Console
{
@@ -48,6 +49,11 @@ namespace Robust.Shared.Console
ConsoleHost.WriteLine(Player, text);
}
public void WriteLine(FormattedMessage message)
{
ConsoleHost.WriteLine(Player, message);
}
/// <inheritdoc />
public void WriteError(string text)
{

View File

@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using JetBrains.Annotations;
@@ -8,7 +9,7 @@ namespace Robust.Shared.Console
/// Basic interface to handle console commands. Any class implementing this will be
/// registered with the console system through reflection.
/// </summary>
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)]
[UsedImplicitly(ImplicitUseTargetFlags.WithInheritors), Obsolete("New commands should utilize RtShellCommand.")]
public interface IConsoleCommand
{
/// <summary>
@@ -78,7 +79,7 @@ namespace Robust.Shared.Console
/// <remarks>
/// If this method is implemented, <see cref="GetCompletion"/> will not be automatically called.
/// </remarks>
ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args, CancellationToken cancel)
ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args, string argStr, CancellationToken cancel)
{
return ValueTask.FromResult(GetCompletion(shell, args));
}

View File

@@ -2,6 +2,7 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Shared.Console
{
@@ -21,7 +22,7 @@ namespace Robust.Shared.Console
/// <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 ValueTask<CompletionResult> ConCommandCompletionAsyncCallback(IConsoleShell shell, string[] args, string argStr);
public delegate void ConAnyCommandCallback(IConsoleShell shell, string commandName, string argStr, string[] args);
@@ -249,6 +250,8 @@ namespace Robust.Shared.Console
/// <param name="text">Text message to send.</param>
void WriteLine(ICommonSession? session, string text);
void WriteLine(ICommonSession? session, FormattedMessage msg);
/// <summary>
/// Sends a foreground colored text string to the remote session.
/// </summary>

View File

@@ -1,4 +1,5 @@
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Shared.Console
{
@@ -54,6 +55,8 @@ namespace Robust.Shared.Console
/// <param name="text">Line of text to write.</param>
void WriteLine(string text);
void WriteLine(FormattedMessage message);
/// <summary>
/// Write an error line to the console window.
/// </summary>

View File

@@ -1,3 +1,4 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Robust.Shared.IoC;
@@ -5,6 +6,7 @@ using Robust.Shared.Localization;
namespace Robust.Shared.Console;
[Obsolete("You should use ToolshedCommand instead.")]
public abstract class LocalizedCommands : IConsoleCommand
{
[Dependency] protected readonly ILocalizationManager LocalizationManager = default!;
@@ -23,12 +25,12 @@ public abstract class LocalizedCommands : IConsoleCommand
/// <inheritdoc />
public abstract void Execute(IConsoleShell shell, string argStr, string[] args);
/// <inheritdoc />
public virtual CompletionResult GetCompletion(IConsoleShell shell, string[] args) => CompletionResult.Empty;
/// <inheritdoc />
public virtual ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args,
public virtual ValueTask<CompletionResult> GetCompletionAsync(IConsoleShell shell, string[] args, string argStr,
CancellationToken cancel)
{
return ValueTask.FromResult(GetCompletion(shell, args));

View File

@@ -1,5 +1,7 @@
using System.IO;
using Lidgren.Network;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
#nullable disable
@@ -9,17 +11,24 @@ namespace Robust.Shared.Network.Messages
{
public override MsgGroups MsgGroup => MsgGroups.String;
public string Text { get; set; }
public FormattedMessage Text { get; set; }
public bool Error { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Text = buffer.ReadString();
int length = buffer.ReadVariableInt32();
using var stream = buffer.ReadAlignedMemory(length);
Text = serializer.Deserialize<FormattedMessage>(stream);
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
{
buffer.Write(Text);
var stream = new MemoryStream();
serializer.Serialize(stream, Text);
buffer.WriteVariableInt32((int)stream.Length);
buffer.Write(stream.AsSpan());
}
}
}

View File

@@ -12,6 +12,8 @@ public sealed class MsgConCompletion : NetMessage
public int Seq { get; set; }
public string[] Args { get; set; }
public string ArgString { get; set; }
public override void ReadFromBuffer(NetIncomingMessage buffer, IRobustSerializer serializer)
{
Seq = buffer.ReadInt32();
@@ -22,6 +24,8 @@ public sealed class MsgConCompletion : NetMessage
{
Args[i] = buffer.ReadString();
}
ArgString = buffer.ReadString();
}
public override void WriteToBuffer(NetOutgoingMessage buffer, IRobustSerializer serializer)
@@ -33,5 +37,7 @@ public sealed class MsgConCompletion : NetMessage
{
buffer.Write(arg);
}
buffer.Write(ArgString);
}
}

View File

@@ -44,11 +44,5 @@ namespace Robust.Shared.Players
/// on the Client only the LocalPlayer has a network channel.
/// </remarks>
INetChannel ConnectedClient { get; }
/// <summary>
/// Porting convenience for admin commands which use such logic as "at the player's feet", etc: the transform component of the attached entity.
/// </summary>
[Obsolete("Query manually from the EntityUid")]
TransformComponent? AttachedEntityTransform => IoCManager.Resolve<IEntityManager>().GetComponentOrNull<TransformComponent>(AttachedEntity);
}
}

View File

@@ -17,6 +17,7 @@ using Robust.Shared.Serialization;
using Robust.Shared.Serialization.Manager;
using Robust.Shared.Threading;
using Robust.Shared.Timing;
using Robust.Shared.Toolshed;
namespace Robust.Shared
{
@@ -46,6 +47,7 @@ namespace Robust.Shared
deps.Register<IVerticesSimplifier, RamerDouglasPeuckerSimplifier>();
deps.Register<IParallelManager, ParallelManager>();
deps.Register<IParallelManagerInternal, ParallelManager>();
deps.Register<ToolshedManager>();
deps.Register<HttpClientHolder>();
}
}

View File

@@ -0,0 +1,90 @@
using System;
using JetBrains.Annotations;
namespace Robust.Shared.Toolshed;
/// <summary>
/// Used to mark a class so that <see cref="ToolshedManager"/> automatically discovers and registers it.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
[MeansImplicitUse]
public sealed class ToolshedCommandAttribute : Attribute
{
public string? Name = null;
}
/// <summary>
/// Marks a function in a <see cref="ToolshedCommand"/> as being an implementation of that command, so that Toolshed will use it's signature for parsing/etc.
/// </summary>
[AttributeUsage(AttributeTargets.Method)]
[MeansImplicitUse]
public sealed class CommandImplementationAttribute : Attribute
{
public readonly string? SubCommand = null;
public CommandImplementationAttribute(string? subCommand = null)
{
SubCommand = subCommand;
}
}
/// <summary>
/// Marks an argument in a function in a <see cref="ToolshedCommand"/> as being the "piped" argument, the return value of the prior command in the chain.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class PipedArgumentAttribute : Attribute
{
}
/// <summary>
/// Marks an argument in a function as being an argument of a <see cref="ToolshedCommand"/>.
/// This will make it so the argument will get parsed.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class CommandArgumentAttribute : Attribute
{
}
/// <summary>
/// Marks an argument in a function as specifying whether or not this call to a <see cref="ToolshedCommand"/> is inverted.
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class CommandInvertedAttribute : Attribute
{
}
/// <summary>
/// Marks an argument in a function as being where the invocation context should be provided in a <see cref="ToolshedCommand"/>.
/// </summary>
/// <seealso cref="IInvocationContext"/>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class CommandInvocationContextAttribute : Attribute
{
}
/// <summary>
/// Marks a command implementation as taking the type of the previous command in sequence as a generic argument.
/// </summary>
/// <remarks>
/// If the argument marked with <see cref="PipedArgumentAttribute"/> is not <c>T</c> but instead a pattern like <c>IEnumerable&lt;T&gt;</c>, Toolshed will account for this.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public sealed class TakesPipedTypeAsGenericAttribute : Attribute
{
}
// Internal because this is just a hack at the moment and should be replaced with proper inference later!
// Overrides type argument parsing to parse a block and then use it's return type as the sole type argument.
internal sealed class MapLikeCommandAttribute : Attribute
{
public bool TakesPipedType;
public MapLikeCommandAttribute(bool takesPipedType = true)
{
TakesPipedType = takesPipedType;
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Robust.Shared.Toolshed.Commands.Debug;
[ToolshedCommand]
internal sealed class FuckCommand : ToolshedCommand
{
[CommandImplementation]
public object? Fuck([PipedArgument] object? value)
{
throw new Exception("fuck!");
}
}

View File

@@ -0,0 +1,18 @@
using System;
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Shared.Toolshed.Commands;
[ToolshedCommand]
internal sealed class EcsCompCommand : ToolshedCommand
{
[Dependency] private readonly IComponentFactory _factory = default!;
[CommandImplementation("listty")]
public IEnumerable<Type> ListTy()
{
return _factory.AllRegisteredTypes;
}
}

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed.TypeParsers;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class CompCommand : ToolshedCommand
{
public override Type[] TypeParameterParsers => new[] {typeof(ComponentType)};
[CommandImplementation]
public IEnumerable<T> CompEnumerable<T>([PipedArgument] IEnumerable<EntityUid> input)
where T: IComponent
{
return input.Where(HasComp<T>).Select(Comp<T>);
}
[CommandImplementation]
public T? CompDirect<T>([PipedArgument] EntityUid input)
where T : IComponent
{
TryComp(input, out T? res);
return res;
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class DeleteCommand : ToolshedCommand
{
[Dependency] private readonly IEntityManager _entity = default!;
[CommandImplementation]
public void Delete([PipedArgument] IEnumerable<EntityUid> entities)
{
foreach (var ent in entities)
{
Del(ent);
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed.Invocation;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class DoCommand : ToolshedCommand
{
private SharedTransformSystem? _xformSys;
[CommandImplementation, TakesPipedTypeAsGeneric]
public void Do<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] string command)
{
if (ctx is not OldShellInvocationContext { } reqCtx)
{
throw new NotImplementedException();
}
_xformSys ??= GetSys<SharedTransformSystem>();
var xformQ = GetEntityQuery<TransformComponent>();
var shell = reqCtx.Shell;
foreach (var i in input)
{
var cmdStr = command;
if (i is EntityUid id)
{
var worldPos = _xformSys.GetWorldPosition(id, xformQ);
cmdStr = cmdStr
.Replace("$ID", id.ToString())
.Replace("$WX", worldPos.X.ToString(CultureInfo.InvariantCulture))
.Replace("$WY", worldPos.Y.ToString(CultureInfo.InvariantCulture));
}
cmdStr = cmdStr.Replace("$SELF", i!.ToString() ?? "");
shell.ExecuteCommand(cmdStr);
}
}
}

View File

@@ -0,0 +1,11 @@
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class EntCommand : ToolshedCommand
{
[CommandImplementation]
public EntityUid Ent([CommandArgument] EntityUid ent) => ent;
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class EntitiesCommand : ToolshedCommand
{
[CommandImplementation]
public IEnumerable<EntityUid> Entities()
{
// NOTE: Makes a copy due to the fact chained on commands might modify this list.
return EntityManager.GetEntities().ToHashSet();
}
}

View File

@@ -0,0 +1,17 @@
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class NamedCommand : ToolshedCommand
{
[CommandImplementation]
public IEnumerable<EntityUid> Named([PipedArgument] IEnumerable<EntityUid> input, [CommandArgument] string regex, [CommandInverted] bool inverted)
{
var compiled = new Regex($"${regex}^");
return input.Where(x => compiled.IsMatch(EntName(x)) ^ inverted);
}
}

View File

@@ -0,0 +1,18 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class NearbyCommand : ToolshedCommand
{
private EntityLookupSystem? _lookup;
[CommandImplementation]
public IEnumerable<EntityUid> Nearby([PipedArgument] IEnumerable<EntityUid> input, [CommandArgument] float range)
{
_lookup ??= GetSys<EntityLookupSystem>();
return input.SelectMany(x => _lookup.GetEntitiesInRange(x, range)).Distinct();
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class PausedCommand : ToolshedCommand
{
[CommandImplementation]
public IEnumerable<EntityUid> Paused([PipedArgument] IEnumerable<EntityUid> entities, [CommandInverted] bool inverted)
{
return entities.Where(x => Comp<MetaDataComponent>(x).EntityPaused ^ inverted);
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class PrototypedCommand : ToolshedCommand
{
[CommandImplementation()]
public IEnumerable<EntityUid> Prototyped([PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] string prototype)
=> input.Where(x => MetaData(x).EntityPrototype?.ID == prototype);
}

View File

@@ -0,0 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed.TypeParsers;
namespace Robust.Shared.Toolshed.Commands.Entities;
[ToolshedCommand]
internal sealed class WithCommand : ToolshedCommand
{
[CommandImplementation]
public IEnumerable<EntityUid> With([PipedArgument] IEnumerable<EntityUid> input, [CommandArgument] ComponentType ty, [CommandInverted] bool inverted)
{
return input.Where(x => EntityManager.HasComponent(x, ty.Ty) ^ inverted);
}
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class AnyCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Any<T>([PipedArgument] IEnumerable<T> input) => input.Any();
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic ;
[ToolshedCommand(Name = "=>")]
internal sealed class ArrowCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Arrow<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T input,
[CommandArgument] ValueRef<T> @ref
)
{
@ref.Set(ctx, input);
return input;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public List<T> Arrow<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] ValueRef<List<T>> @ref
)
{
var list = input.ToList();
@ref.Set(ctx, list);
return list;
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class AsCommand : ToolshedCommand
{
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
[CommandImplementation, TakesPipedTypeAsGeneric]
public TOut? As<TOut, TIn>([PipedArgument] TIn value)
=> (TOut?)(object?)value;
}

View File

@@ -0,0 +1,14 @@
using System.Collections.Generic;
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class CountCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public int Count<T>([PipedArgument] IEnumerable<T> enumerable)
{
return enumerable.Count();
}
}

View File

@@ -0,0 +1,157 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using Robust.Shared.GameObjects;
using Robust.Shared.Players;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand, MapLikeCommand(false)]
internal sealed class EmplaceCommand : ToolshedCommand
{
[CommandImplementation]
IEnumerable<TOut> Emplace<TIn, TOut>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<TIn> value,
[CommandArgument] Block<TOut> block
)
{
foreach (var v in value)
{
var emplaceCtx = new EmplaceContext<TIn>(ctx, v, EntityManager);
yield return block.Invoke(null, emplaceCtx)!;
}
}
}
internal record struct EmplaceContext<T>(IInvocationContext Inner, T Value, IEntityManager EntityManager) : IInvocationContext
{
public bool CheckInvokable(CommandSpec command, out IConError? error)
{
return Inner.CheckInvokable(command, out error);
}
public ICommonSession? Session => Inner.Session;
public void WriteLine(string line)
{
Inner.WriteLine(line);
}
public void ReportError(IConError err)
{
Inner.ReportError(err);
}
public IEnumerable<IConError> GetErrors()
{
return Inner.GetErrors();
}
public void ClearErrors()
{
Inner.ClearErrors();
}
public Dictionary<string, object?> Variables => default!; // we never use this.
public IEnumerable<string> GetVars()
{
// note: this lies.
return Inner.GetVars();
}
public object? ReadVar(string name)
{
if (name == "value")
return Value;
if (Value is IEmplaceBreakout breakout)
{
if (breakout.TryReadVar(name, out var value))
return value;
}
// TODO: Emplace behavior should be generalized and not hardcoded.
if (Value is EntityUid id)
{
switch (name)
{
case "wy":
case "wx":
{
var xform = EntityManager.GetComponent<TransformComponent>(id);
var sys = EntityManager.System<SharedTransformSystem>();
var coords = sys.GetWorldPosition(xform);
if (name == "wx")
return coords.X;
else
return coords.Y;
}
case "proto":
case "desc":
case "name":
case "paused":
{
var meta = EntityManager.GetComponent<MetaDataComponent>(id);
switch (name)
{
case "proto":
return meta.EntityPrototype?.ID ?? "";
case "desc":
return meta.EntityDescription;
case "name":
return meta.EntityName;
case "paused":
return meta.EntityPaused;
}
throw new UnreachableException();
}
}
}
else if (Value is ICommonSession session)
{
switch (name)
{
case "ent":
{
return session.AttachedEntity!;
}
case "name":
{
return session.Name;
}
case "userid":
{
return session.UserId;
}
}
}
return Inner.ReadVar(name);
}
public void WriteVar(string name, object? value)
{
if (name == "value")
return;
if (Value is IEmplaceBreakout v)
{
if (v.VarsOverriden.Contains(name))
return;
}
Inner.WriteVar(name, value);
}
}
public interface IEmplaceBreakout
{
public ImmutableHashSet<string> VarsOverriden { get; }
public bool TryReadVar(string name, out object? value);
}

View File

@@ -0,0 +1,11 @@
using System.Collections.Generic;
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class FirstCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T First<T>([PipedArgument] IEnumerable<T> input) => input.First();
}

View File

@@ -0,0 +1,22 @@
using System.Collections;
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class IsEmptyCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool IsEmpty<T>([PipedArgument] T? input, [CommandInverted] bool inverted)
{
if (input is null)
return true ^ inverted; // Null is empty for all we care.
if (input is IEnumerable @enum)
{
return !@enum.Cast<object?>().Any() ^ inverted;
}
return false ^ inverted; // Not a collection, cannot be empty.
}
}

View File

@@ -0,0 +1,8 @@
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class IsNullCommand : ToolshedCommand
{
[CommandImplementation]
public bool IsNull([PipedArgument] object? input, [CommandInverted] bool inverted) => input is null ^ inverted;
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand, MapLikeCommand]
internal sealed class MapCommand : ToolshedCommand
{
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<TOut>? Map<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<TIn> value,
[CommandArgument] Block<TIn, TOut> block
)
{
return value.Select(x => block.Invoke(x, ctx)).Where(x => x != null).Cast<TOut>();
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.IoC;
using Robust.Shared.Random;
using Robust.Shared.Toolshed.TypeParsers;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class SelectCommand : ToolshedCommand
{
[Dependency] private readonly IRobustRandom _random = default!;
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<TR> Select<TR>([PipedArgument] IEnumerable<TR> enumerable, [CommandArgument] Quantity quantity, [CommandInverted] bool inverted)
{
var arr = enumerable.ToArray();
_random.Shuffle(arr);
if (quantity is {Amount: { } amount})
{
var taken = (int) System.Math.Ceiling(amount);
if (inverted)
taken = System.Math.Max(0, arr.Length - taken);
return arr.Take(taken);
}
else
{
var percent = inverted
? (int) System.Math.Floor(arr.Length * System.Math.Clamp(1 - (double) quantity.Percentage!, 0, 1))
: (int) System.Math.Floor(arr.Length * System.Math.Clamp((double) quantity.Percentage!, 0, 1));
return arr.Take(percent);
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class SplatCommand : ToolshedCommand
{
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
[CommandImplementation]
public IEnumerable<T> Splat<T>(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] ValueRef<T> value,
[CommandArgument] ValueRef<int> amountValue)
{
var amount = amountValue.Evaluate(ctx);
for (var i = 0; i < amount; i++)
{
yield return value.Evaluate(ctx)!;
if (ctx.GetErrors().Any())
yield break;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class UniqueCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Unique<T>([PipedArgument] IEnumerable<T> input)
=> input.Distinct();
}

View File

@@ -0,0 +1,16 @@
using System;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class ValCommand : ToolshedCommand
{
public override Type[] TypeParameterParsers => new[] {typeof(Type)};
[CommandImplementation]
public T Val<T>(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] ValueRef<T> value
) => value.Evaluate(ctx)!;
}

View File

@@ -0,0 +1,28 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Generic;
[ToolshedCommand]
internal sealed class WhereCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Where<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, bool> check
)
{
foreach (var i in input)
{
var res = check.Invoke(i, ctx);
if (ctx.GetErrors().Any())
yield break;
if (res)
yield return i;
}
}
}

View File

@@ -0,0 +1,166 @@
using System.Numerics;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Math;
[ToolshedCommand(Name = "+")]
public sealed class AddCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IAdditionOperators<T, T, T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x + yVal;
}
}
[ToolshedCommand(Name = "-")]
public sealed class SubtractCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : ISubtractionOperators<T, T, T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x - yVal;
}
}
[ToolshedCommand(Name = "*")]
public sealed class MultiplyCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IMultiplyOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x * yVal;
}
}
[ToolshedCommand(Name = "/")]
public sealed class DivideCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IDivisionOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x / yVal;
}
}
[ToolshedCommand]
public sealed class MinCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IComparisonOperators<T, T, bool>
{
var yVal = y.Evaluate(ctx)!;
return x > yVal ? yVal : x;
}
}
[ToolshedCommand]
public sealed class MaxCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IComparisonOperators<T, T, bool>
{
var yVal = y.Evaluate(ctx)!;
return x > yVal ? x : yVal;
}
}
[ToolshedCommand(Name = "&")]
public sealed class BitAndCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x & yVal;
}
}
[ToolshedCommand(Name = "|")]
public sealed class BitOrCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x | yVal;
}
}
[ToolshedCommand(Name = "^")]
public sealed class BitXorCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x ^ yVal;
}
}
[ToolshedCommand]
public sealed class NegCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x
)
where T : IUnaryNegationOperators<T, T>
{
return -x;
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Numerics;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Math;
[ToolshedCommand(Name = ">")]
public sealed class GreaterThanCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumber<T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return x > yVal;
}
}
[ToolshedCommand(Name = "<")]
public sealed class LessThanCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IComparisonOperators<T, T, bool>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return x > yVal;
}
}
[ToolshedCommand(Name = ">=")]
public sealed class GreaterThanOrEqualCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumber<T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return x >= yVal;
}
}
[ToolshedCommand(Name = "<=")]
public sealed class LessThanOrEqualCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IComparisonOperators<T, T, bool>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return x <= yVal;
}
}
[ToolshedCommand(Name = "==")]
public sealed class EqualCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IEquatable<T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return x.Equals(yVal);
}
}
[ToolshedCommand(Name = "!=")]
public sealed class NotEqualCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public bool Comparison<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IEquatable<T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return !x.Equals(yVal);
}
}

View File

@@ -0,0 +1,27 @@
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class BuildInfoCommand : ToolshedCommand
{
[Dependency] private readonly IConfigurationManager _cfg = default!;
private static readonly string Gold = Color.Gold.ToHex();
[CommandImplementation]
public void BuildInfo([CommandInvocationContext] IInvocationContext ctx)
{
var game = _cfg.GetCVar(CVars.BuildForkId);
ctx.WriteLine(FormattedMessage.FromMarkup($"[color={Gold}]Game:[/color] {game}"));
var buildCommit = _cfg.GetCVar(CVars.BuildHash);
ctx.WriteLine(FormattedMessage.FromMarkup($"[color={Gold}]Build commit:[/color] {buildCommit}"));
var buildManifest = _cfg.GetCVar(CVars.BuildManifestHash);
ctx.WriteLine(FormattedMessage.FromMarkup($"[color={Gold}]Manifest hash:[/color] {buildManifest}"));
var engine = _cfg.GetCVar(CVars.BuildEngineVersion);
ctx.WriteLine(FormattedMessage.FromMarkup($"[color={Gold}]Engine ver:[/color] {engine}"));
}
}

View File

@@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class CmdCommand : ToolshedCommand
{
[CommandImplementation("list")]
public IEnumerable<CommandSpec> List()
=> Toolshed.AllCommands();
[CommandImplementation("moo")]
public string Moo()
=> "Have you mooed today?";
[CommandImplementation("descloc")]
public string GetLogStr([PipedArgument] CommandSpec cmd) => cmd.DescLocStr();
#if CLIENT_SCRIPTING
[CommandImplementation("getshim")]
public MethodInfo GetShim([CommandArgument] Block block)
{
// this is gross sue me
var invocable = block.CommandRun.Commands.Last().Item1.Invocable;
return invocable.GetMethodInfo();
}
#endif
}

View File

@@ -0,0 +1,21 @@
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class ExplainCommand : ToolshedCommand
{
[CommandImplementation]
public void Explain(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] CommandRun expr
)
{
foreach (var (cmd, _) in expr.Commands)
{
ctx.WriteLine(cmd.Command.GetHelp(cmd.SubCommand));
ctx.WriteLine($"{cmd.PipedType?.PrettyName() ?? "[none]"} -> {cmd.ReturnType?.PrettyName() ?? "[none]"}");
ctx.WriteLine("");
}
}
}

View File

@@ -0,0 +1,28 @@
using Robust.Shared.Maths;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class HelpCommand : ToolshedCommand
{
private static readonly string Gold = Color.Gold.ToHex();
private static readonly string Aqua = Color.Aqua.ToHex();
[CommandImplementation]
public void Help([CommandInvocationContext] IInvocationContext ctx)
{
ctx.WriteLine($@"
TOOLSHED
/.\\\\\\\\
/___\\\\\\\\
|''''|'''''|
| 8 | === |
|_0__|_____|");
ctx.WriteMarkup($@"
For a list of commands, run [color={Gold}]cmd:list[/color].
To search for commands, run [color={Gold}]cmd:list search ""[color={Aqua}]query[/color]""[/color].
For a breakdown of how a string of commands flows, run [color={Gold}]explain [color={Aqua}]commands here[/color][/color].
For help with old console commands, run [color={Gold}]oldhelp[/color].
");
}
}

View File

@@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using Robust.Shared.IoC;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class IoCCommand : ToolshedCommand
{
[CommandImplementation("registered")]
public IEnumerable<Type> Registered() => IoCManager.Instance!.GetRegisteredTypes();
[CommandImplementation("get")]
public object? Get([PipedArgument] Type t) => IoCManager.ResolveType(t);
}

View File

@@ -0,0 +1,17 @@
using Robust.Shared.Localization;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class LocCommand : ToolshedCommand
{
[CommandImplementation("tryloc")]
public string? TryLocalize([PipedArgument] string str)
{
Loc.TryGetString(str, out var loc);
return loc;
}
[CommandImplementation("loc")]
public string Localize([PipedArgument] string str) => Loc.GetString(str);
}

View File

@@ -0,0 +1,48 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.GameObjects;
using Robust.Shared.Physics.Components;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class PhysicsCommand : ToolshedCommand
{
private SharedTransformSystem? _xform = default!;
[CommandImplementation("velocity")]
public IEnumerable<float> Velocity([PipedArgument] IEnumerable<EntityUid> input)
{
var physQuery = GetEntityQuery<PhysicsComponent>();
foreach (var ent in input)
{
if (!physQuery.TryGetComponent(ent, out var comp))
continue;
yield return comp.LinearVelocity.Length();
}
}
[CommandImplementation("parent")]
public IEnumerable<EntityUid> Parent([PipedArgument] IEnumerable<EntityUid> input)
{
_xform ??= GetSys<SharedTransformSystem>();
return input.Select(x => Comp<TransformComponent>(x).ParentUid);
}
[CommandImplementation("angular_velocity")]
public IEnumerable<float> AngularVelocity([PipedArgument] IEnumerable<EntityUid> input)
{
var physQuery = GetEntityQuery<PhysicsComponent>();
foreach (var ent in input)
{
if (!physQuery.TryGetComponent(ent, out var comp))
continue;
yield return comp.AngularVelocity;
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class SearchCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<FormattedMessage> Search<T>([PipedArgument] IEnumerable<T> input, [CommandArgument] string term)
{
var list = input.Select(x => Toolshed.PrettyPrintType(x)).ToList();
return list.Where(x => x.Contains(term, StringComparison.InvariantCultureIgnoreCase)).Select(x =>
{
var startIdx = x.IndexOf(term, StringComparison.InvariantCultureIgnoreCase);
return ConHelpers.HighlightSpan(x, (startIdx, startIdx + term.Length), Color.Aqua);
});
}
}

View File

@@ -0,0 +1,19 @@
using Robust.Shared.Maths;
using Robust.Shared.Timing;
using Robust.Shared.Toolshed.Syntax;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class StopwatchCommand : ToolshedCommand
{
[CommandImplementation]
public object? Stopwatch([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] CommandRun expr)
{
var watch = new Stopwatch();
watch.Start();
var result = expr.Invoke(null, ctx);
ctx.WriteMarkup($"Ran expression in [color={Color.Aqua.ToHex()}]{watch.Elapsed:g}[/color]");
return result;
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class TypesCommand : ToolshedCommand
{
[CommandImplementation("consumers")]
public void Consumers([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] object? input)
{
var t = input is Type ? (Type)input : input!.GetType();
ctx.WriteLine($"Valid intakers for {t.PrettyName()}:");
foreach (var (command, subCommand) in Toolshed.CommandsTakingType(t))
{
if (subCommand is null)
ctx.WriteLine($"{command.Name}");
else
ctx.WriteLine($"{command.Name}:{subCommand}");
}
}
[CommandImplementation("tree")]
public IEnumerable<Type> Tree([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] object? input)
{
var t = input is Type ? (Type)input : input!.GetType();
return Toolshed.AllSteppedTypes(t);
}
[CommandImplementation("gettype")]
public Type GetType([PipedArgument] object? input)
{
return input?.GetType() ?? typeof(void);
}
[CommandImplementation("fullname")]
public string FullName([PipedArgument] Type input)
{
return input.FullName!;
}
}

View File

@@ -0,0 +1,13 @@
using System.Linq;
namespace Robust.Shared.Toolshed.Commands.Misc;
[ToolshedCommand]
internal sealed class VarsCommand : ToolshedCommand
{
[CommandImplementation]
public void Vars([CommandInvocationContext] IInvocationContext ctx)
{
ctx.WriteLine(Toolshed.PrettyPrintType(ctx.GetVars().Select(x => $"{x} = {ctx.ReadVar(x)}")));
}
}

View File

@@ -0,0 +1,25 @@
using Robust.Shared.GameObjects;
using Robust.Shared.Toolshed.Errors;
namespace Robust.Shared.Toolshed.Commands.Players;
[ToolshedCommand]
internal sealed class SelfCommand : ToolshedCommand
{
[CommandImplementation]
public EntityUid Self([CommandInvocationContext] IInvocationContext ctx)
{
if (ctx.Session is null)
{
ctx.ReportError(new NotForServerConsoleError());
return default!;
}
if (ctx.Session.AttachedEntity is { } ent)
return ent;
ctx.ReportError(new SessionHasNoEntityError(ctx.Session));
return default!;
}
}

View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Reflection;
namespace Robust.Shared.Toolshed.Commands.Types;
#if CLIENT_SCRIPTING
[ToolshedCommand]
internal sealed class MethodsCommand : ToolshedCommand
{
[CommandImplementation("get")]
public IEnumerable<MethodInfo> Get([PipedArgument] IEnumerable<Type> types)
{
foreach (var ty in types)
{
foreach (var method in ty.GetMethods())
{
yield return method;
}
}
}
[CommandImplementation("overrides")]
public IEnumerable<MethodInfo> Overrides([PipedArgument] IEnumerable<Type> types)
{
foreach (var ty in types)
{
foreach (var method in ty.GetMethods())
{
if (method.DeclaringType != ty)
yield return method;
}
}
}
[CommandImplementation("overridesfrom")]
public IEnumerable<MethodInfo> OverridesFrom([PipedArgument] IEnumerable<Type> types, [CommandArgument] Type t)
{
foreach (var ty in types)
{
foreach (var method in ty.GetMethods())
{
if (method.DeclaringType == t)
yield return method;
}
}
}
}
#endif

View File

@@ -0,0 +1,27 @@
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Commands.Vfs;
[ToolshedCommand]
internal sealed class CdCommand : VfsCommand
{
[CommandImplementation]
public void Cd(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] ResPath path
)
{
var curPath = CurrentPath(ctx);
if (path.IsRooted)
{
curPath = path;
}
else
{
curPath /= path;
}
SetPath(ctx, curPath);
}
}

View File

@@ -0,0 +1,31 @@
using System.Collections.Generic;
using System.Linq;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Commands.Vfs;
[ToolshedCommand]
internal sealed class LsCommand : VfsCommand
{
[CommandImplementation("here")]
public IEnumerable<ResPath> LsHere([CommandInvocationContext] IInvocationContext ctx)
{
var curPath = CurrentPath(ctx);
return Resources.ContentGetDirectoryEntries(curPath).Select(x => curPath/x);
}
[CommandImplementation("in")]
public IEnumerable<ResPath> LsIn([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] ResPath @in)
{
var curPath = CurrentPath(ctx);
if (@in.IsRooted)
{
curPath = @in;
}
else
{
curPath /= @in;
}
return Resources.ContentGetDirectoryEntries(curPath).Select(x => curPath/x);
}
}

View File

@@ -0,0 +1,29 @@
using System;
using JetBrains.Annotations;
using Robust.Shared.ContentPack;
using Robust.Shared.IoC;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Commands.Vfs;
/// <summary>
/// A simple base class for commands that work with the VFS and would like to manipulate the user's current location within the VFS.
/// </summary>
/// <seealso cref="UserVfsLocVariableName"/>
[PublicAPI]
public abstract class VfsCommand : ToolshedCommand
{
[Dependency] protected readonly IResourceManager Resources = default!;
/// <summary>
/// The name of the variable storing a <see cref="ResPath">ResPath?</see> representing the user's current VFS location.
/// </summary>
public const string UserVfsLocVariableName = "user_vfs_loc";
protected ResPath CurrentPath(IInvocationContext ctx) => ((ResPath?) ctx.ReadVar(UserVfsLocVariableName)) ?? ResPath.Root;
protected void SetPath(IInvocationContext ctx, ResPath path)
{
ctx.WriteVar(UserVfsLocVariableName, (ResPath?) path.Clean());
}
}

View File

@@ -0,0 +1,123 @@
using System.Diagnostics;
using System.Text;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Errors;
/// <summary>
/// A Toolshed-oriented representation of an error.
/// Contains metadata about where in an executed command it occurred, and supports formatting.
/// <code>
/// > entities runverbas self "yeet"
/// entities runverbas self "yeet"
/// ^^^^^
/// You must be logged in with a client to use this, the server console isn't workable.
/// </code>
/// </summary>
public interface IConError
{
/// <summary>
/// Returns a user friendly description of the error.
/// </summary>
/// <remarks>
/// This calls <see cref="M:Robust.Shared.Toolshed.Errors.IConError.DescribeInner"/> for the actual description by default.
/// If you fully override this, you should provide your own context provider, as the default implementation includes where in the expression the error occurred.
/// </remarks>
public FormattedMessage Describe()
{
var msg = new FormattedMessage();
if (Expression is { } expr && IssueSpan is { } span)
{
msg.AddMessage(ConHelpers.HighlightSpan(expr, span, Color.Red));
msg.PushNewline();
msg.AddMessage(ConHelpers.ArrowSpan(span));
msg.PushNewline();
}
msg.AddMessage(DescribeInner());
#if TOOLS
if (Trace is not null)
{
msg.PushNewline();
msg.AddText(Trace.ToString());
}
#endif
return msg;
}
/// <summary>
/// Describes the error, called by <see cref="M:Robust.Shared.Toolshed.Errors.IConError.Describe"/>'s default implementation.
/// </summary>
protected FormattedMessage DescribeInner();
/// <summary>
/// The expression this error was raised in or on.
/// </summary>
public string? Expression { get; protected set; }
/// <summary>
/// Where in the expression this error was raised.
/// </summary>
public Vector2i? IssueSpan { get; protected set; }
/// <summary>
/// The stack trace for this error if any.
/// </summary>
/// <remarks>
/// This is not present in release builds.
/// </remarks>
public StackTrace? Trace { get; protected set; }
/// <summary>
/// Attaches additional context to an error, namely where it occurred.
/// </summary>
/// <param name="expression">Expression the error occured in or on.</param>
/// <param name="issueSpan">Where in the expression it occurred.</param>
public void Contextualize(string expression, Vector2i issueSpan)
{
if (Expression is not null && IssueSpan is not null)
return;
#if TOOLS
Trace = new StackTrace(skipFrames: 1);
#endif
Expression = expression;
IssueSpan = issueSpan;
}
}
/// <summary>
/// Pile of helpers for console formatting.
/// </summary>
public static class ConHelpers
{
/// <summary>
/// Highlights a section of the input a given color.
/// </summary>
/// <param name="input">Input text.</param>
/// <param name="span">Span to highlight.</param>
/// <param name="color">Color to use.</param>
/// <returns>A formatted message with highlighting applied.</returns>
public static FormattedMessage HighlightSpan(string input, Vector2i span, Color color)
{
if (span.X == span.Y)
return new FormattedMessage();
var msg = FormattedMessage.FromMarkup(input[..span.X]);
msg.PushColor(color);
msg.AddText(input[span.X..span.Y]);
msg.Pop();
msg.AddText(input[span.Y..]);
return msg;
}
/// <summary>
/// Creates a string with up arrows (<c>^</c>) under the given span.
/// </summary>
/// <param name="span">Span to underline.</param>
/// <returns>A string of whitespace with (<c>^</c>) under the given span.</returns>
public static FormattedMessage ArrowSpan(Vector2i span)
{
var builder = new StringBuilder();
builder.Append(' ', span.X);
builder.Append('^', span.Y - span.X);
return FormattedMessage.FromMarkup(builder.ToString());
}
}

View File

@@ -0,0 +1,18 @@
using System.Diagnostics;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Errors;
public sealed class NotForServerConsoleError : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup(
"You must be logged in with a client to use this, the server console isn't workable.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,18 @@
using System.Diagnostics;
using Robust.Shared.Maths;
using Robust.Shared.Players;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Errors;
public record struct SessionHasNoEntityError(ICommonSession Session) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup($"The user {Session.Name} has no attached entity.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Diagnostics;
using JetBrains.Annotations;
using Robust.Shared.Maths;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Errors;
public sealed class UnhandledExceptionError : IConError
{
[PublicAPI]
public Exception Exception;
public FormattedMessage DescribeInner()
{
var msg = new FormattedMessage();
msg.AddText(Exception.ToString());
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
public UnhandledExceptionError(Exception exception)
{
Exception = exception;
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Diagnostics;
using System.Text;
using JetBrains.Annotations;
using Robust.Shared.IoC;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed;
public sealed class ForwardParser
{
[Dependency] public readonly ToolshedManager Toolshed = default!;
public readonly string Input;
public int MaxIndex { get; private set; }
public int Index { get; private set; } = 0;
public ForwardParser(string input, ToolshedManager toolshed)
{
Toolshed = toolshed;
Input = input;
MaxIndex = input.Length - 1;
}
private ForwardParser(ForwardParser parser, int sliceSize, int? index)
{
IoCManager.InjectDependencies(this);
DebugTools.Assert(sliceSize > 0);
Input = parser.Input;
Index = index ?? parser.Index;
MaxIndex = Math.Min(parser.MaxIndex, Index + sliceSize - 1);
}
public bool SpanInRange(int length)
{
return MaxIndex >= (Index + length - 1);
}
public bool EatMatch(char c)
{
if (PeekChar() == c)
{
Index++;
return true;
}
return false;
}
public char? PeekChar()
{
if (!SpanInRange(1))
return null;
return Input[Index];
}
public char? GetChar()
{
if (PeekChar() is { } c)
{
Index++;
return c;
}
return null;
}
[PublicAPI]
public void DebugPrint()
{
Logger.DebugS("parser", Input);
MakeDebugPointer(Index);
MakeDebugPointer(MaxIndex, '|');
}
private void MakeDebugPointer(int pointAt, char pointer = '^')
{
var builder = new StringBuilder();
builder.Append(' ', pointAt);
builder.Append(pointer);
Logger.DebugS("parser", builder.ToString());
}
private string? MaybeGetWord(bool advanceIndex, Func<char, bool>? test)
{
var startingIndex = Index;
test ??= static c => c != ' ';
var builder = new StringBuilder();
Consume(char.IsWhiteSpace);
// Walk forward until we run into whitespace
while (PeekChar() is { } c && test(c))
{
builder.Append(GetChar());
}
if (startingIndex == Index)
return null;
if (!advanceIndex)
Index = startingIndex;
return builder.ToString();
}
public string? PeekWord(Func<char, bool>? test = null) => MaybeGetWord(false, test);
public string? GetWord(Func<char, bool>? test = null) => MaybeGetWord(true, test);
public ParserRestorePoint Save()
{
return new ParserRestorePoint(Index);
}
public void Restore(ParserRestorePoint point)
{
Index = point.Index;
}
public int Consume(Func<char, bool> control)
{
var amount = 0;
while (PeekChar() is { } c && control(c))
{
GetChar();
amount++;
}
return amount;
}
public ForwardParser? SliceBlock(char startDelim, char endDelim)
{
var checkpoint = Save();
Consume(char.IsWhiteSpace);
if (GetChar() != startDelim)
{
Restore(checkpoint);
return null;
}
var blockStart = Index;
var stack = 1;
while (stack > 0)
{
var c = GetChar();
if (c == startDelim)
stack++;
if (c == endDelim)
{
if (--stack == 0)
break;
}
if (c == null)
{
Restore(checkpoint);
return null;
}
}
return new ForwardParser(this, Index - blockStart, blockStart);
}
}
public readonly record struct ParserRestorePoint(int Index);
public record struct OutOfInputError : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup("Ran out of input data when data was expected.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,152 @@
using System.Collections.Generic;
using Robust.Shared.Players;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed;
/// <summary>
/// A context in which Toolshed commands can be run using <see cref="M:Robust.Shared.Toolshed.ToolshedManager.InvokeCommand(Robust.Shared.Toolshed.IInvocationContext,System.String,System.Object,System.Object@)"/>.
/// </summary>
public interface IInvocationContext
{
/// <summary>
/// Test if the given command spec can be invoked in this context.
/// </summary>
/// <param name="command">Command to test.</param>
/// <param name="error">An error to report, if any.</param>
/// <returns>Whether or not the given command can be invoked</returns>
/// <remarks>
/// THIS IS A SECURITY BOUNDARY.
/// If you want to avoid players being able to just reboot your server, you should probably implement this!
/// </remarks>
public bool CheckInvokable(CommandSpec command, out IConError? error);
/// <summary>
/// The session this context is for, if any.
/// </summary>
ICommonSession? Session { get; }
/// <summary>
/// Writes a line to this context's output.
/// </summary>
/// <param name="line">The text to print.</param>
/// <remarks>
/// This can be stubbed safely, there's no requirement that the side effects of this function be observable.
/// </remarks>
public void WriteLine(string line);
/// <summary>
/// Writes a formatted message to this context's output.
/// </summary>
/// <param name="line">The formatted message to print.</param>
/// <remarks>
/// This can be stubbed safely, there's no requirement that the side effects of this function be observable.
/// </remarks>
public void WriteLine(FormattedMessage line)
{
// Cut markup for server.
if (Session is null)
{
WriteLine(line.ToString());
return;
}
WriteLine(line.ToMarkup());
}
/// <summary>
/// Writes the given markup to this context's output.
/// </summary>
/// <param name="markup">The markup to print.</param>
/// <remarks>
/// This can be stubbed safely, there's no requirement that the side effects of this function be observable.
/// </remarks>
public void WriteMarkup(string markup)
{
WriteLine(FormattedMessage.FromMarkup(markup));
}
/// <summary>
/// Writes the given error to the context's output.
/// </summary>
/// <param name="error">The error to write out.</param>
/// <remarks>
/// This can be stubbed safely, there's no requirement that the side effects of this function be observable.
/// </remarks>
public void WriteError(IConError error)
{
WriteLine(error.Describe());
}
/// <summary>
/// Reports the given error to the context.
/// </summary>
/// <param name="err">Error to report.</param>
/// <remarks>
/// This may have arbitrary side effects. Usually, it'll push the error to some list you can retrieve with GetErrors().
/// </remarks>
public void ReportError(IConError err);
/// <summary>
/// Gets the list of unobserved errors.
/// </summary>
/// <returns>An enumerable of console errors.</returns>
/// <remarks>
/// This is not required to contain anything, may contain errors you did not ReportError(), and may not contain errors you did ReportError().
/// </remarks>
public IEnumerable<IConError> GetErrors();
/// <summary>
/// Clears the list of unobserved errors.
/// </summary>
/// <remarks>
/// After calling this, assuming atomicity (no threads), GetErrors() MUST be empty in order for an IInvocationContext to be compliant.
/// </remarks>
public void ClearErrors();
/// <summary>
/// The backing variable storage.
/// </summary>
/// <remarks>
/// You don't have to use this at all.
/// </remarks>
protected Dictionary<string, object?> Variables { get; }
/// <summary>
/// Reads the given variable from the context.
/// </summary>
/// <param name="name">The name of the variable.</param>
/// <returns>The contents of the variable.</returns>
/// <remarks>
/// This may behave arbitrarily, but it's advised it behave somewhat sanely.
/// </remarks>
public virtual object? ReadVar(string name)
{
Variables.TryGetValue(name, out var res);
return res;
}
/// <summary>
/// Writes the given variable to the context.
/// </summary>
/// <param name="name">The name of the variable.</param>
/// <param name="value">The contents of the variable.</param>
/// <remarks>
/// Writes may be ignored or manipulated.
/// </remarks>
public virtual void WriteVar(string name, object? value)
{
Variables[name] = value;
}
/// <summary>
/// Provides a list of all variables that have been written to at some point.
/// </summary>
/// <returns>List of all variables.</returns>
public virtual IEnumerable<string> GetVars()
{
return Variables.Keys;
}
}

View File

@@ -0,0 +1,10 @@
using JetBrains.Annotations;
using Robust.Shared.Players;
using Robust.Shared.Toolshed.Errors;
namespace Robust.Shared.Toolshed;
public interface IPermissionController
{
public bool CheckInvokable(CommandSpec command, ICommonSession? user, out IConError? error);
}

View File

@@ -0,0 +1,73 @@
using System.Collections.Generic;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Players;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Invocation;
/// <inheritdoc />
internal sealed class OldShellInvocationContext : IInvocationContext
{
[Dependency] private readonly ToolshedManager _toolshed = default!;
private readonly List<IConError> _errors = new();
/// <summary>
/// Old system's shell associated with this context
/// </summary>
public IConsoleShell Shell;
public OldShellInvocationContext(IConsoleShell shell)
{
IoCManager.InjectDependencies(this);
Shell = shell;
}
/// <inheritdoc />
public bool CheckInvokable(CommandSpec command, out IConError? error)
{
if (_toolshed.ActivePermissionController is { } controller)
return controller.CheckInvokable(command, Session, out error);
error = null;
return true;
}
/// <inheritdoc />
public ICommonSession? Session => Shell.Player;
/// <inheritdoc />
public void WriteLine(string line)
{
Shell.WriteLine(line);
}
/// <inheritdoc />
public void WriteLine(FormattedMessage line)
{
Shell.WriteLine(line);
}
/// <inheritdoc />
public void ReportError(IConError err)
{
_errors.Add(err);
}
/// <inheritdoc />
public IEnumerable<IConError> GetErrors()
{
return _errors;
}
/// <inheritdoc />
public void ClearErrors()
{
_errors.Clear();
}
/// <inheritdoc />
public Dictionary<string, object?> Variables { get; } = new();
}

View File

@@ -0,0 +1,294 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Robust.Shared.Exceptions;
using Robust.Shared.Log;
namespace Robust.Shared.Toolshed;
// TODO: Audit this for sandboxability and expose some of these to content.
internal static class ReflectionExtensions
{
public static bool CanBeNull(this Type t)
{
return !t.IsValueType || t.IsGenericType(typeof(Nullable<>));
}
public static bool CanBeEmpty(this Type t)
{
return t.CanBeNull() || t.IsGenericType(typeof(IEnumerable<>));
}
public static bool IsGenericType(this Type t, Type genericType)
{
return t.IsGenericType && t.GetGenericTypeDefinition() == genericType;
}
public static IEnumerable<Type> GetVariants(this Type t, ToolshedManager toolshed)
{
var args = t.GetGenericArguments();
var generic = t.GetGenericTypeDefinition();
var genericArgs = generic.GetGenericArguments();
var variantCount = genericArgs.Count(x => (x.GenericParameterAttributes & GenericParameterAttributes.VarianceMask) != 0);
if (variantCount > 1)
{
throw new NotImplementedException("I swear to god I am NOT supporting more than one variant type parameter. Absolutely no combinatorial explosions in this house, factorials can go home.");
}
yield return t;
if (variantCount < 1)
{
yield break;
}
var variant = 0;
for (var i = 0; i < args.Length; i++)
{
if ((genericArgs[i].GenericParameterAttributes & GenericParameterAttributes.VarianceMask) != 0)
{
variant = i;
break;
}
}
var newArgs = (Type[]) args.Clone();
foreach (var type in toolshed.AllSteppedTypes(args[variant], false))
{
newArgs[variant] = type;
yield return generic.MakeGenericType(newArgs);
}
}
public static IEnumerable<(string, List<MethodInfo>)> BySubCommand(this IEnumerable<MethodInfo> methods)
{
var output = new Dictionary<string, List<MethodInfo>>();
foreach (var method in methods)
{
var subCommand = method.GetCustomAttribute<CommandImplementationAttribute>()!.SubCommand ?? "";
if (!output.TryGetValue(subCommand, out var methodList))
{
methodList = new();
output[subCommand] = methodList;
}
methodList.Add(method);
}
return output.Select(x => (x.Key, x.Value));
}
public static Type StepDownConstraints(this Type t)
{
if (!t.IsGenericType || t.IsGenericTypeDefinition)
return t;
var oldArgs = t.GenericTypeArguments;
var newArgs = new Type[oldArgs.Length];
for (var i = 0; i < oldArgs.Length; i++)
{
if (oldArgs[i].IsGenericType)
newArgs[i] = oldArgs[i].GetGenericTypeDefinition();
else
newArgs[i] = oldArgs[i];
}
return t.GetGenericTypeDefinition().MakeGenericType(newArgs);
}
public static string PrettyName(this Type type)
{
var name = type.Name;
if (type.IsGenericParameter)
return type.ToString();
if (type.DeclaringType is not null)
{
name = $"{PrettyName(type.DeclaringType!)}.{type.Name}";
}
if (type.GetGenericArguments().Length == 0)
{
return name;
}
if (!name.Contains('`'))
return name + "<>";
var genericArguments = type.GetGenericArguments();
var exactName = name.Substring(0, name.IndexOf('`', StringComparison.InvariantCulture));
return exactName + "<" + string.Join(",", genericArguments.Select(PrettyName)) + ">";
}
public static ParameterInfo? ConsoleGetPipedArgument(this MethodInfo method)
{
var p = method.GetParameters().Where(x => x.GetCustomAttribute<PipedArgumentAttribute>() is not null).ToList();
return p.FirstOrDefault();
}
public static IEnumerable<ParameterInfo> ConsoleGetArguments(this MethodInfo method)
{
return method.GetParameters().Where(x => x.GetCustomAttribute<CommandArgumentAttribute>() is not null);
}
public static Expression CreateEmptyExpr(this Type t)
{
if (!t.CanBeEmpty())
throw new TypeArgumentException();
if (t.IsGenericType(typeof(IEnumerable<>)))
{
var array = Array.CreateInstance(t.GetGenericArguments().First(), 0);
return Expression.Constant(array, t);
}
if (t.CanBeNull())
{
if (Nullable.GetUnderlyingType(t) is not null)
return Expression.Constant(t.GetConstructor(BindingFlags.CreateInstance, Array.Empty<Type>())!.Invoke(null, null), t);
return Expression.Constant(null, t);
}
throw new NotImplementedException();
}
// IEnumerable<EntityUid> ^ IEnumerable<T> -> EntityUid
public static Type Intersect(this Type left, Type right)
{
if (!left.IsGenericType)
return left;
if (!right.IsGenericType)
return left;
return left.GetGenericArguments().First();
}
public static void DumpGenericInfo(this Type t)
{
Logger.Debug($"Info for {t.PrettyName()}");
Logger.Debug(
$"GP {t.IsGenericParameter} | MP {t.IsGenericMethodParameter} | TP {t.IsGenericTypeParameter} | DEF {t.IsGenericTypeDefinition} | TY {t.IsGenericType} | CON {t.IsConstructedGenericType}");
if (t.IsGenericParameter)
Logger.Debug($"CONSTRAINTS: {string.Join(", ", t.GetGenericParameterConstraints().Select(PrettyName))}");
if (!t.IsGenericTypeDefinition && IsGenericRelated(t) && t.IsGenericType)
DumpGenericInfo(t.GetGenericTypeDefinition());
foreach (var p in t.GetGenericArguments())
{
DumpGenericInfo(p);
}
}
public static bool IsAssignableToGeneric(this Type left, Type right, ToolshedManager toolshed, bool recursiveDescent = true)
{
if (left.IsAssignableTo(right))
return true;
if (right.IsInterface)
{
foreach (var i in left.GetInterfaces())
{
if (left.IsAssignableToGeneric(i, toolshed, recursiveDescent))
return true;
}
}
if (left.Constructable() && right.IsGenericParameter)
{
var constraints = right.GetGenericParameterConstraints();
foreach (var t in constraints)
{
if (!left.IsAssignableToGeneric(t, toolshed, recursiveDescent))
return false;
}
return true;
}
if (left.IsGenericType && right.IsGenericType && left.GenericTypeArguments.Length == right.GenericTypeArguments.Length)
{
var equal = left.GetGenericTypeDefinition() == right.GetGenericTypeDefinition();
if (!equal)
goto next;
var res = true;
foreach (var (leftTy, rightTy) in left.GenericTypeArguments.Zip(right.GenericTypeArguments))
{
res &= leftTy.IsAssignableToGeneric(rightTy, toolshed, false);
}
return res;
}
next:
if (recursiveDescent)
{
foreach (var leftSubTy in toolshed.AllSteppedTypes(left))
{
if (leftSubTy.IsAssignableToGeneric(right, toolshed, false))
{
return true;
}
}
}
return false;
}
public static bool IsGenericRelated(this Type t)
{
return t.IsGenericParameter | t.IsGenericType | t.IsGenericMethodParameter | t.IsGenericTypeDefinition | t.IsConstructedGenericType | t.IsGenericTypeParameter;
}
public static bool Constructable(this Type t)
{
if (!IsGenericRelated(t))
return true;
if (!t.IsGenericType)
return false;
var r = true;
foreach (var arg in t.GetGenericArguments())
{
r &= Constructable(arg);
}
return r;
}
public static PropertyInfo? FindIndexerProperty(
this Type type)
{
var defaultPropertyAttribute = type.GetCustomAttributes<DefaultMemberAttribute>().FirstOrDefault();
return defaultPropertyAttribute == null
? null
: type.GetRuntimeProperties()
.FirstOrDefault(
pi =>
pi.Name == defaultPropertyAttribute.MemberName
&& pi.IsIndexerProperty()
&& pi.SetMethod?.GetParameters() is { } parameters
&& parameters.Length == 2
&& parameters[0].ParameterType == typeof(string));
}
public static bool IsIndexerProperty(this PropertyInfo propertyInfo)
{
var indexParams = propertyInfo.GetIndexParameters();
return indexParams.Length == 1
&& indexParams[0].ParameterType == typeof(string);
}
}

View File

@@ -0,0 +1,155 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
public sealed class Block
{
internal CommandRun CommandRun { get; set; }
public static bool TryParse(
bool doAutoComplete,
ForwardParser parser,
Type? pipedType,
[NotNullWhen(true)] out Block? block,
out ValueTask<(CompletionResult?, IConError?)>? autoComplete,
out IConError? error
)
{
parser.Consume(char.IsWhiteSpace);
var enclosed = parser.EatMatch('{');
CommandRun.TryParse(enclosed, doAutoComplete, parser, pipedType, null, !enclosed, out var expr, out autoComplete, out error);
if (expr is null)
{
block = null;
return false;
}
if (enclosed && !parser.EatMatch('}'))
{
error = new MissingClosingBrace();
block = null;
return false;
}
block = new Block(expr);
return true;
}
public Block(CommandRun expr)
{
CommandRun = expr;
}
public object? Invoke(object? input, IInvocationContext ctx)
{
return CommandRun.Invoke(input, ctx);
}
}
/// <summary>
/// Something more akin to actual expressions.
/// </summary>
public sealed class Block<T>
{
internal CommandRun<T> CommandRun { get; set; }
public static bool TryParse(bool doAutoComplete, ForwardParser parser, Type? pipedType,
[NotNullWhen(true)] out Block<T>? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error)
{
parser.Consume(char.IsWhiteSpace);
var enclosed = parser.EatMatch('{');
CommandRun<T>.TryParse(enclosed, doAutoComplete, parser, pipedType, !enclosed, out var expr, out autoComplete, out error);
if (expr is null)
{
block = null;
return false;
}
if (enclosed && !parser.EatMatch('}'))
{
error = new MissingClosingBrace();
block = null;
return false;
}
block = new Block<T>(expr);
return true;
}
public Block(CommandRun<T> expr)
{
CommandRun = expr;
}
public T? Invoke(object? input, IInvocationContext ctx)
{
return CommandRun.Invoke(input, ctx);
}
}
public sealed class Block<TIn, TOut>
{
internal CommandRun<TIn, TOut> CommandRun { get; set; }
public static bool TryParse(bool doAutoComplete, ForwardParser parser, Type? pipedType,
[NotNullWhen(true)] out Block<TIn, TOut>? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error)
{
parser.Consume(char.IsWhiteSpace);
var enclosed = parser.EatMatch('{');
CommandRun<TIn, TOut>.TryParse(enclosed, doAutoComplete, parser, !enclosed, out var expr, out autoComplete, out error);
if (expr is null)
{
block = null;
return false;
}
if (enclosed && !parser.EatMatch('}'))
{
error = new MissingClosingBrace();
block = null;
return false;
}
block = new Block<TIn, TOut>(expr);
return true;
}
public Block(CommandRun<TIn, TOut> expr)
{
CommandRun = expr;
}
public TOut? Invoke(object? input, IInvocationContext ctx)
{
return CommandRun.Invoke(input, ctx);
}
}
public record struct MissingClosingBrace() : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup("Expected a closing brace.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.Log;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
/// <summary>
/// A "run" of commands. Not a true expression.
/// </summary>
public sealed class CommandRun
{
public readonly List<(ParsedCommand, Vector2i)> Commands;
private readonly string _originalExpr;
public static bool TryParse(
bool blockMode,
bool doAutocomplete,
ForwardParser parser,
Type? pipedType,
Type? targetOutput,
bool once,
[NotNullWhen(true)] out CommandRun? expr,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete,
out IConError? error
)
{
autocomplete = null;
error = null;
var cmds = new List<(ParsedCommand, Vector2i)>();
var start = parser.Index;
var noCommand = false;
parser.Consume(char.IsWhiteSpace);
while ((!once || cmds.Count < 1) && ParsedCommand.TryParse(doAutocomplete, parser, pipedType, out var cmd, out error, out noCommand, out autocomplete, targetOutput))
{
var end = parser.Index;
pipedType = cmd.ReturnType;
cmds.Add((cmd, (start, end)));
parser.Consume(char.IsWhiteSpace);
start = parser.Index;
if (blockMode && parser.PeekChar() == '}')
break;
}
if (error is OutOfInputError && noCommand)
error = null;
if (error is not null and not OutOfInputError || error is OutOfInputError && !noCommand || cmds.Count == 0)
{
expr = null;
return false;
}
if (!(cmds.Last().Item1.ReturnType?.IsAssignableTo(targetOutput) ?? false) && targetOutput is not null)
{
error = new ExpressionOfWrongType(targetOutput, cmds.Last().Item1.ReturnType!, once);
expr = null;
return false;
}
expr = new CommandRun(cmds, parser.Input);
return true;
}
public object? Invoke(object? input, IInvocationContext ctx, bool reportErrors = true)
{
var ret = input;
foreach (var (cmd, span) in Commands)
{
ret = cmd.Invoke(ret, ctx);
if (ctx.GetErrors().Any())
{
// Got an error, we need to report it and break out.
foreach (var err in ctx.GetErrors())
{
err.Contextualize(_originalExpr, span);
ctx.WriteLine(err.Describe());
}
return null;
}
}
return ret;
}
private CommandRun(List<(ParsedCommand, Vector2i)> commands, string originalExpr)
{
Commands = commands;
_originalExpr = originalExpr;
}
}
public sealed class CommandRun<TIn, TOut>
{
internal readonly CommandRun InnerCommandRun;
public static bool TryParse(bool blockMode, bool doAutoComplete, ForwardParser parser, bool once,
[NotNullWhen(true)] out CommandRun<TIn, TOut>? expr,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete, out IConError? error)
{
if (!CommandRun.TryParse(blockMode, doAutoComplete, parser, typeof(TIn), typeof(TOut), once, out var innerExpr, out autocomplete, out error))
{
expr = null;
return false;
}
expr = new CommandRun<TIn, TOut>(innerExpr);
return true;
}
public TOut? Invoke(object? input, IInvocationContext ctx)
{
var res = InnerCommandRun.Invoke(input, ctx);
if (res is null)
return default;
return (TOut?) res;
}
private CommandRun(CommandRun commandRun)
{
InnerCommandRun = commandRun;
}
}
public sealed class CommandRun<TRes>
{
internal readonly CommandRun _innerCommandRun;
public static bool TryParse(bool blockMode, bool doAutoComplete, ForwardParser parser, Type? pipedType, bool once,
[NotNullWhen(true)] out CommandRun<TRes>? expr, out ValueTask<(CompletionResult?, IConError?)>? completion,
out IConError? error)
{
if (!CommandRun.TryParse(blockMode, doAutoComplete, parser, pipedType, typeof(TRes), once, out var innerExpr, out completion, out error))
{
expr = null;
return false;
}
expr = new CommandRun<TRes>(innerExpr);
return true;
}
public TRes? Invoke(object? input, IInvocationContext ctx)
{
var res = _innerCommandRun.Invoke(input, ctx);
if (res is null)
return default;
return (TRes?) res;
}
private CommandRun(CommandRun commandRun)
{
_innerCommandRun = commandRun;
}
}
public record struct ExpressionOfWrongType(Type Expected, Type Got, bool Once) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = FormattedMessage.FromMarkup(
$"Expected an expression of type {Expected.PrettyName()}, but got {Got.PrettyName()}");
if (Once)
{
msg.PushNewline();
msg.AddText("Note: A single command is expected here, if you were trying to chain commands please surround the run with { } to form a block.");
}
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,329 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
using Invocable = Func<CommandInvocationArguments, object?>;
public sealed class ParsedCommand
{
public ToolshedCommand Command { get; }
public Type? ReturnType { get; }
public Type? PipedType => Bundle.PipedArgumentType;
internal Invocable Invocable { get; }
private CommandArgumentBundle Bundle { get; }
public string? SubCommand { get; }
public static bool TryParse(
bool doAutoComplete,
ForwardParser parser,
Type? pipedArgumentType,
[NotNullWhen(true)] out ParsedCommand? result,
out IConError? error,
out bool noCommand,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete,
Type? targetType = null
)
{
noCommand = false;
var checkpoint = parser.Save();
var bundle = new CommandArgumentBundle()
{Arguments = new(), Inverted = false, PipedArgumentType = pipedArgumentType, TypeArguments = Array.Empty<Type>()};
autocomplete = null;
if (!TryDigestModifiers(parser, bundle, out error)
|| !TryParseCommand(doAutoComplete, parser, bundle, pipedArgumentType, targetType, out var subCommand, out var invocable, out var command, out error, out noCommand, out autocomplete)
|| !command.TryGetReturnType(subCommand, pipedArgumentType, bundle.TypeArguments, out var retType)
)
{
result = null;
parser.Restore(checkpoint);
return false;
}
result = new(bundle, invocable, command, retType, subCommand);
return true;
}
private ParsedCommand(CommandArgumentBundle bundle, Invocable invocable, ToolshedCommand command, Type? returnType, string? subCommand)
{
Invocable = invocable;
Bundle = bundle;
Command = command;
ReturnType = returnType;
SubCommand = subCommand;
}
private static bool TryDigestModifiers(ForwardParser parser, CommandArgumentBundle bundle, out IConError? error)
{
error = null;
if (parser.PeekWord() == "not")
{
parser.GetWord(); //yum
bundle.Inverted = true;
}
return true;
}
private static bool TryParseCommand(
bool makeCompletions,
ForwardParser parser,
CommandArgumentBundle bundle,
Type? pipedType,
Type? targetType,
out string? subCommand,
[NotNullWhen(true)] out Invocable? invocable,
[NotNullWhen(true)] out ToolshedCommand? command,
out IConError? error,
out bool noCommand,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete
)
{
noCommand = false;
var start = parser.Index;
var cmd = parser.GetWord(c => c is not ':' and not '{' and not '}' && !char.IsWhiteSpace(c));
subCommand = null;
invocable = null;
command = null;
if (cmd is null)
{
if (parser.PeekChar() is null)
{
noCommand = true;
error = new OutOfInputError();
error.Contextualize(parser.Input, (parser.Index, parser.Index));
autocomplete = null;
if (makeCompletions)
{
var cmds = parser.Toolshed.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
return false;
}
else
{
noCommand = true;
error = new NotValidCommandError(targetType);
error.Contextualize(parser.Input, (start, parser.Index));
autocomplete = null;
return false;
}
}
if (!parser.Toolshed.TryGetCommand(cmd, out var cmdImpl))
{
error = new UnknownCommandError(cmd);
error.Contextualize(parser.Input, (start, parser.Index));
autocomplete = null;
if (makeCompletions)
{
var cmds = parser.Toolshed.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
return false;
}
if (cmdImpl.HasSubCommands)
{
error = null;
autocomplete = null;
if (makeCompletions)
{
var cmds = parser.Toolshed.CommandsTakingType(pipedType ?? typeof(void)).Where(x => x.Cmd.Name == cmd);
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((
CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), error));
}
if (parser.GetChar() is not ':')
{
error = new OutOfInputError();
error.Contextualize(parser.Input, (parser.Index, parser.Index));
return false;
}
var subCmdStart = parser.Index;
if (parser.GetWord() is not { } subcmd)
{
error = new OutOfInputError();
error.Contextualize(parser.Input, (parser.Index, parser.Index));
return false;
}
if (!cmdImpl.Subcommands.Contains(subcmd))
{
error = new UnknownSubcommandError(cmd, subcmd, cmdImpl);
error.Contextualize(parser.Input, (subCmdStart, parser.Index));
return false;
}
subCommand = subcmd;
}
if (parser.Consume(char.IsWhiteSpace) == 0 && makeCompletions)
{
error = null;
var cmds = parser.Toolshed.CommandsTakingType(pipedType ?? typeof(void));
autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), "<command>"), null));
return false;
}
var argsStart = parser.Index;
if (!cmdImpl.TryParseArguments(makeCompletions, parser, pipedType, subCommand, out var args, out var types, out error, out autocomplete))
{
error?.Contextualize(parser.Input, (argsStart, parser.Index));
return false;
}
bundle.TypeArguments = types;
if (!cmdImpl.TryGetImplementation(bundle.PipedArgumentType, subCommand, types, out var impl))
{
error = new NoImplementationError(cmd, types, subCommand, bundle.PipedArgumentType);
error.Contextualize(parser.Input, (start, parser.Index));
autocomplete = null;
return false;
}
bundle.Arguments = args;
invocable = impl;
command = cmdImpl;
autocomplete = null;
return true;
}
private bool _passedInvokeTest = false;
public object? Invoke(object? pipedIn, IInvocationContext ctx)
{
if (!_passedInvokeTest && !ctx.CheckInvokable(new CommandSpec(Command, SubCommand), out var error))
{
// Could not invoke the command for whatever reason, i.e. permission errors.
if (error is not null)
ctx.ReportError(error);
return null;
}
// TODO: This optimization might be dangerous if blocks can be passed to other people through vars.
// Or not if it can only be done deliberately, but social engineering is a thing.
_passedInvokeTest = true;
try
{
return Invocable.Invoke(new CommandInvocationArguments()
{Bundle = Bundle, PipedArgument = pipedIn, Context = ctx});
}
catch (Exception e)
{
ctx.ReportError(new UnhandledExceptionError(e));
return null;
}
}
}
public record struct UnknownCommandError(string Cmd) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup($"Got unknown command {Cmd}.");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record struct NoImplementationError(string Cmd, Type[] Types, string? SubCommand, Type? PipedType) : IConError
{
public FormattedMessage DescribeInner()
{
var newCon = IoCManager.Resolve<ToolshedManager>();
var msg = FormattedMessage.FromMarkup($"Could not find an implementation for {Cmd} given the input type {PipedType?.PrettyName() ?? "void"}.");
msg.PushNewline();
var typeArgs = "";
if (Types.Length != 0)
{
typeArgs = "<" + string.Join(",", Types.Select(ReflectionExtensions.PrettyName)) + ">";
}
msg.AddText($"Signature: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {PipedType?.PrettyName() ?? "void"} -> ???");
var piped = PipedType ?? typeof(void);
var cmdImpl = newCon.GetCommand(Cmd);
var accepted = cmdImpl.AcceptedTypes(SubCommand).ToHashSet();
foreach (var (command, subCommand) in newCon.CommandsTakingType(piped))
{
if (!command.TryGetReturnType(subCommand, piped, Array.Empty<Type>(), out var retType) || !accepted.Any(x => retType.IsAssignableTo(x)))
continue;
if (!cmdImpl.TryGetReturnType(SubCommand, retType, Types, out var myRetType))
continue;
msg.PushNewline();
msg.AddText($"The command {command.Name}{(subCommand is not null ? $":{subCommand}" : "")} can convert from {piped.PrettyName()} to {retType.PrettyName()}.");
msg.PushNewline();
msg.AddText($"With this fix, the new signature will be: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {retType?.PrettyName() ?? "void"} -> {myRetType?.PrettyName() ?? "void"}.");
}
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record struct UnknownSubcommandError(string Cmd, string SubCmd, ToolshedCommand Command) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = new FormattedMessage();
msg.AddText($"The command group {Cmd} doesn't have command {SubCmd}.");
msg.PushNewline();
msg.AddText($"The valid commands are: {string.Join(", ", Command.Subcommands)}.");
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
public record struct NotValidCommandError(Type? TargetType) : IConError
{
public FormattedMessage DescribeInner()
{
var msg = new FormattedMessage();
msg.AddText("Ran into an invalid command, could not parse.");
if (TargetType is not null && TargetType != typeof(void))
{
msg.PushNewline();
msg.AddText($"The parser was trying to obtain a run of type {TargetType.PrettyName()}, make sure your run actually returns that value.");
}
return msg;
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,106 @@
using System;
using System.Diagnostics;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
public sealed class ValueRef<T> : ValueRef<T, T>
{
public ValueRef(ValueRef<T, T> inner)
{
InnerBlock = inner.InnerBlock;
VarName = inner.VarName;
HasValue = inner.HasValue;
Value = inner.Value;
Expression = inner.Expression;
RefSpan = inner.RefSpan;
}
public ValueRef(string varName) : base(varName)
{
}
public ValueRef(Block<T> innerBlock) : base(innerBlock)
{
}
public ValueRef(T value) : base(value)
{
}
}
[Virtual]
public class ValueRef<T, TAuto>
{
internal Block<T>? InnerBlock;
internal string? VarName;
internal bool HasValue = false;
internal T? Value;
internal string? Expression;
internal Vector2i? RefSpan;
protected ValueRef()
{
}
public ValueRef(string varName)
{
VarName = varName;
}
public ValueRef(Block<T> innerBlock)
{
InnerBlock = innerBlock;
}
public ValueRef(T value)
{
Value = value;
HasValue = true;
}
public bool LikelyConst => VarName is not null || HasValue;
public T? Evaluate(IInvocationContext ctx)
{
if (Value is not null && HasValue)
{
return Value;
}
else if (VarName is not null)
{
return (T?)ctx.ReadVar(VarName);
}
else if (InnerBlock is not null)
{
return InnerBlock.Invoke(null, ctx);
}
else
{
throw new UnreachableException();
}
}
public void Set(IInvocationContext ctx, T? value)
{
if (VarName is null)
throw new NotImplementedException();
ctx.WriteVar(VarName!, value);
}
}
public record struct BadVarTypeError(Type Got, Type Expected, string VarName) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup(
$"Got unexpected type {Got.PrettyName()} in {VarName}, expected {Expected.PrettyName()}");
}
public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}

View File

@@ -0,0 +1,113 @@
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using JetBrains.Annotations;
using Robust.Shared.GameObjects;
namespace Robust.Shared.Toolshed;
public abstract partial class ToolshedCommand
{
[PublicAPI, IoC.Dependency]
protected readonly IEntityManager EntityManager = default!;
[PublicAPI, IoC.Dependency]
protected readonly IEntitySystemManager EntitySystemManager = default!;
/// <summary>
/// A shorthand for retrieving <see cref="MetaDataComponent"/> for an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected MetaDataComponent MetaData(EntityUid entity)
=> EntityManager.GetComponent<MetaDataComponent>(entity);
/// <summary>
/// A shorthand for retrieving <see cref="TransformComponent"/> for an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected TransformComponent Transform(EntityUid entity)
=> EntityManager.GetComponent<TransformComponent>(entity);
/// <summary>
/// A shorthand for retrieving an entity's name.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected string EntName(EntityUid entity)
=> EntityManager.GetComponent<MetaDataComponent>(entity).EntityName;
/// <summary>
/// A shorthand for deleting an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected void Del(EntityUid entityUid)
=> EntityManager.DeleteEntity(entityUid);
/// <summary>
/// A shorthand for checking if an entity is deleted or otherwise non-existant.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool Deleted(EntityUid entity)
=> EntityManager.Deleted(entity);
/// <summary>
/// A shorthand for retrieving the given component for an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T Comp<T>(EntityUid entity)
where T: IComponent
=> EntityManager.GetComponent<T>(entity);
/// <summary>
/// A shorthand for checking if an entity has the given component.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool HasComp<T>(EntityUid entityUid)
where T: IComponent
=> EntityManager.HasComponent<T>(entityUid);
/// <summary>
/// A shorthand for attempting to retrieve the given component for an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryComp<T>(EntityUid? entity, [NotNullWhen(true)] out T? component)
where T: IComponent
=> EntityManager.TryGetComponent(entity, out component);
/// <inheritdoc cref="TryComp{T}(System.Nullable{Robust.Shared.GameObjects.EntityUid},out T?)"/>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryComp<T>(EntityUid entity, [NotNullWhen(true)] out T? component)
where T: IComponent
=> EntityManager.TryGetComponent(entity, out component);
/// <summary>
/// A shorthand for adding a component to the given entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T AddComp<T>(EntityUid entity)
where T : Component, new()
=> EntityManager.AddComponent<T>(entity);
/// <summary>
/// A shorthand for ensuring an entity has the given component.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T EnsureComp<T>(EntityUid entity)
where T: Component, new()
=> EntityManager.EnsureComponent<T>(entity);
/// <summary>
/// A shorthand for retrieving an entity system.
/// </summary>
/// <remarks>This may be replaced with some form of dependency attribute in the future.</remarks>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T GetSys<T>()
where T: EntitySystem
=> EntitySystemManager.GetEntitySystem<T>();
/// <summary>
/// A shorthand for retrieving an entity query.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected EntityQuery<T> GetEntityQuery<T>()
where T : Component
=> EntityManager.GetEntityQuery<T>();
}

Some files were not shown because too many files have changed in this diff Show More