From 9af119f57a2e51b5afa80ca58d692da4e6a32740 Mon Sep 17 00:00:00 2001 From: Leon Friedrich <60421075+ElectroJr@users.noreply.github.com> Date: Sat, 21 Dec 2024 17:49:11 +1100 Subject: [PATCH] Toolshed Rejig (#5455) * Toolshed Rejig * shorten hint string * Try fix conflicts. Ill make with work later * bodge * Fix ProtoIdTypeParser assert * comment * AllEntities * Remove more linq from WhereCommand * better help strings * Add ContainsCommand * loc strings * Add contains command description * Add $self variable * Errors for writing to readonly variables * A --- RELEASE-NOTES.md | 2 +- Resources/Locale/en-US/toolshed-commands.ftl | 14 +- Robust.Server/Console/ServerConsoleHost.cs | 19 +- .../Commands/Players/PlayersCommand.cs | 9 +- Robust.Shared.Scripting/ILCommand.cs | 3 +- .../ScriptGlobalsShared.cs | 22 +- Robust.Shared/Console/CompletionResult.cs | 2 +- Robust.Shared/GameObjects/Entity.cs | 34 +- .../GameObjects/EntityManager.Components.cs | 80 ++ Robust.Shared/GameObjects/EntityUid.cs | 12 +- .../GameObjects/IEntityManager.Components.cs | 26 + Robust.Shared/GameObjects/NetEntity.cs | 28 +- .../EntityLookupSystem.ComponentQueries.cs | 1 - .../GameObjects/Systems/EntityLookupSystem.cs | 4 - Robust.Shared/Prototypes/EntProtoId.cs | 5 +- Robust.Shared/Prototypes/ProtoId.cs | 9 +- Robust.Shared/Toolshed/Attributes.cs | 50 +- .../Entities/Components/CompCommand.cs | 3 +- .../Commands/Entities/DeleteCommand.cs | 11 +- .../Toolshed/Commands/Entities/DoCommand.cs | 4 +- .../Commands/Entities/EntitiesCommand.cs | 26 +- .../Commands/Entities/NamedCommand.cs | 2 +- .../Commands/Entities/NearbyCommand.cs | 6 +- .../Commands/Entities/PrototypedCommand.cs | 5 +- .../Commands/Entities/ReplaceCommand.cs | 8 +- .../Commands/Entities/SpawnCommand.cs | 50 +- .../Toolshed/Commands/Entities/WithCommand.cs | 19 +- .../Commands/Entities/World/PosCommand.cs | 2 +- .../Commands/Entities/World/TpCommand.cs | 50 +- .../Toolshed/Commands/Generic/AsCommand.cs | 4 +- .../Commands/Generic/ContainsCommand.cs | 14 + .../Commands/Generic/EmplaceCommand.cs | 340 +++++--- .../Commands/Generic/IterateCommand.cs | 18 +- .../Generic/ListGeneration/RepeatCommand.cs | 9 +- .../Generic/ListGeneration/ToCommand.cs | 10 +- .../Toolshed/Commands/Generic/MapCommand.cs | 22 +- .../Generic/Ordering/SortByCommand.cs | 10 +- .../Commands/Generic/Ordering/SortCommand.cs | 7 +- .../Generic/Ordering/SortDownByCommand.cs | 10 +- .../Generic/Ordering/SortDownCommand.cs | 6 +- .../Generic/Ordering/SortMapByCommand.cs | 10 +- .../Generic/Ordering/SortMapDownByCommand.cs | 10 +- .../Commands/Generic/ReduceCommand.cs | 103 ++- .../Commands/Generic/SelectCommand.cs | 2 +- .../Toolshed/Commands/Generic/TakeCommand.cs | 9 +- .../Toolshed/Commands/Generic/TeeCommand.cs | 23 +- .../Generic/Variables/ArrowCommand.cs | 16 +- .../Commands/Generic/Variables/VarsCommand.cs | 2 +- .../Toolshed/Commands/Generic/WhereCommand.cs | 7 +- .../Commands/Math/ArithmeticCommands.cs | 444 +++------- .../Commands/Math/ComparisonCommands.cs | 68 +- .../Commands/Math/FloatCoreCommands.cs | 46 +- .../Toolshed/Commands/Math/ListCommands.cs | 28 +- .../Commands/Math/MiscOperatorCommands.cs | 24 +- .../Toolshed/Commands/Math/NumAsCommand.cs | 10 +- .../Commands/Math/NumberQuestionCommands.cs | 4 +- .../Toolshed/Commands/Math/PowCommand.cs | 23 +- .../Toolshed/Commands/Math/RngCommand.cs | 29 +- .../Toolshed/Commands/Math/RootCommands.cs | 40 +- .../Commands/Misc/BuildInfoCommand.cs | 2 +- .../Toolshed/Commands/Misc/CmdCommand.cs | 8 +- .../Toolshed/Commands/Misc/ExplainCommand.cs | 38 +- .../Toolshed/Commands/Misc/MoreCommand.cs | 2 +- .../Toolshed/Commands/Misc/SearchCommand.cs | 2 +- .../Commands/Misc/StopwatchCommand.cs | 2 +- .../Toolshed/Commands/Misc/TypesCommand.cs | 4 +- .../Toolshed/Commands/Players/SelfCommand.cs | 2 +- .../Toolshed/Commands/Types/MethodsCommand.cs | 2 +- .../Commands/Values/ConstantCommands.cs | 8 +- .../Toolshed/Commands/Values/EntCommand.cs | 4 +- .../Toolshed/Commands/Values/ValCommand.cs | 9 +- .../Toolshed/Commands/Values/VarCommand.cs | 20 + .../Toolshed/Commands/Vfs/CdCommand.cs | 5 +- .../Toolshed/Commands/Vfs/LsCommand.cs | 4 +- Robust.Shared/Toolshed/Errors/IConError.cs | 23 + Robust.Shared/Toolshed/IInvocationContext.cs | 41 +- .../Toolshed/IPermissionController.cs | 3 +- .../Invocation/OldShellInvocationContext.cs | 42 +- .../Toolshed/LocalVarInvocationContext.cs | 98 +++ .../Toolshed/ReflectionExtensions.cs | 97 ++- Robust.Shared/Toolshed/Syntax/Block.cs | 184 ++-- Robust.Shared/Toolshed/Syntax/Expression.cs | 280 +++++-- .../Toolshed/Syntax/IVariableParser.cs | 153 ++++ .../Toolshed/Syntax/ParsedCommand.cs | 341 ++++---- .../Toolshed/Syntax/ParserContext.Config.cs | 38 +- .../Toolshed/Syntax/ParserContext.cs | 253 ++++-- Robust.Shared/Toolshed/Syntax/ValueRef.cs | 161 ++-- .../Toolshed/ToolshedCommand.Entities.cs | 4 + .../Toolshed/ToolshedCommand.Help.cs | 82 +- .../ToolshedCommand.Implementations.cs | 137 +-- Robust.Shared/Toolshed/ToolshedCommand.cs | 294 ++++--- .../Toolshed/ToolshedCommandImplementor.cs | 784 +++++++++++++----- Robust.Shared/Toolshed/ToolshedEnvironment.cs | 197 +++-- .../Toolshed/ToolshedManager.Parsing.cs | 176 +++- .../Toolshed/ToolshedManager.Permissions.cs | 17 +- .../Toolshed/ToolshedManager.Types.cs | 37 +- Robust.Shared/Toolshed/ToolshedManager.cs | 59 +- .../Toolshed/TypeParsers/BlockType.cs | 89 ++ .../Toolshed/TypeParsers/BlockTypeParser.cs | 63 +- .../Toolshed/TypeParsers/BoolTypeParser.cs | 41 +- .../TypeParsers/CommandRunTypeParser.cs | 47 ++ .../TypeParsers/CommandSpecTypeParser.cs | 78 +- .../TypeParsers/ComponentTypeParser.cs | 32 +- .../Toolshed/TypeParsers/EntityTypeParser.cs | 248 +++++- .../Toolshed/TypeParsers/EnumTypeParser.cs | 30 +- .../TypeParsers/ExpressionTypeParser.cs | 68 -- .../TypeParsers/InstanceIdTypeParser.cs | 10 +- .../TypeParsers/Math/AngleTypeParser.cs | 37 +- .../TypeParsers/Math/ColorTypeParser.cs | 47 +- .../TypeParsers/Math/NumberBaseTypeParser.cs | 45 +- .../TypeParsers/Math/SpanLikeTypeParser.cs | 61 +- .../Toolshed/TypeParsers/ProtoIdTypeParser.cs | 59 -- .../TypeParsers/PrototypeTypeParser.cs | 131 ++- .../TypeParsers/QuantityTypeParser.cs | 24 +- .../Toolshed/TypeParsers/ResPathTypeParser.cs | 25 +- .../Toolshed/TypeParsers/SessionTypeParser.cs | 19 +- .../Toolshed/TypeParsers/StringTypeParser.cs | 103 +-- .../TypeParsers/Tuples/BaseTupleTypeParser.cs | 27 +- .../Toolshed/TypeParsers/TypeParser.cs | 79 +- .../Toolshed/TypeParsers/TypeTypeParser.cs | 54 +- .../TypeParsers/ValueRefTypeParser.cs | 171 ++-- .../Toolshed/TypeParsers/VarRefType.cs | 72 ++ .../Toolshed/TypeParsers/VarRefTypeParser.cs | 91 ++ Robust.Shared/Utility/TypeHelpers.cs | 5 + .../Shared/GameObjects/GenericEntityPrint.cs | 5 +- .../Shared/Toolshed/ArithmeticTest.cs | 2 +- Robust.UnitTesting/Shared/Toolshed/LocTest.cs | 27 +- .../Shared/Toolshed/TestCommands.cs | 90 ++ .../Toolshed/ToolshedParserTest.Bugcheck.cs | 33 +- .../Toolshed/ToolshedParserTest.Core.cs | 1 - .../Shared/Toolshed/ToolshedParserTest.cs | 69 +- .../Shared/Toolshed/ToolshedTest.cs | 202 ++++- .../Shared/Toolshed/ToolshedTests.cs | 298 +++++++ .../Shared/Toolshed/ToolshedValidationTest.cs | 206 +++++ 134 files changed, 5129 insertions(+), 3027 deletions(-) create mode 100644 Robust.Shared/Toolshed/Commands/Generic/ContainsCommand.cs create mode 100644 Robust.Shared/Toolshed/Commands/Values/VarCommand.cs create mode 100644 Robust.Shared/Toolshed/LocalVarInvocationContext.cs create mode 100644 Robust.Shared/Toolshed/Syntax/IVariableParser.cs create mode 100644 Robust.Shared/Toolshed/TypeParsers/BlockType.cs create mode 100644 Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs delete mode 100644 Robust.Shared/Toolshed/TypeParsers/ExpressionTypeParser.cs delete mode 100644 Robust.Shared/Toolshed/TypeParsers/ProtoIdTypeParser.cs create mode 100644 Robust.Shared/Toolshed/TypeParsers/VarRefType.cs create mode 100644 Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs create mode 100644 Robust.UnitTesting/Shared/Toolshed/TestCommands.cs create mode 100644 Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs create mode 100644 Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 344b1397e..ca650b6e5 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -35,7 +35,7 @@ END TEMPLATE--> ### Breaking changes -*None yet* +* Some toolshed command syntax/parsing has changed slightly, and several toolshed related classes and interfaces have changed significantly, including ToolshedManager, type parsers, invocation contexts, and parser contexts. For more detail see the the description of PR #5455 ### New features diff --git a/Resources/Locale/en-US/toolshed-commands.ftl b/Resources/Locale/en-US/toolshed-commands.ftl index 0ec32fd9b..08330dcff 100644 --- a/Resources/Locale/en-US/toolshed-commands.ftl +++ b/Resources/Locale/en-US/toolshed-commands.ftl @@ -1,4 +1,8 @@ -command-description-tpto = +command-help-usage = + Usage: +command-help-invertible = + The behaviour of this command can be inverted using the "not" prefix. +command-description-tpto = Teleport the given entities to some target entity. command-description-player-list = Returns a list of all player sessions. @@ -19,7 +23,7 @@ command-description-buildinfo = 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. + Explains the given expression, providing command descriptions and signatures. This only works for valid expressions, it can't explain commands that it fails to parse. command-description-search = Searches through the input for the provided value. command-description-stopwatch = @@ -53,10 +57,8 @@ 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 = @@ -95,6 +97,8 @@ 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-contains = + Returns whether the input enumerable contains the specified value. command-description-ArrowCommand = Assigns the input to a variable. command-description-isempty = @@ -119,6 +123,8 @@ 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-var = + Returns the contents of the given variable. This will attempt to automatically infer a variables type. Compound commands that modify a variable may need to use the 'val' command instead. command-description-actor-controlled = Filters entities by whether or not they're actively controlled. command-description-actor-session = diff --git a/Robust.Server/Console/ServerConsoleHost.cs b/Robust.Server/Console/ServerConsoleHost.cs index fa9502500..1bc0469e4 100644 --- a/Robust.Server/Console/ServerConsoleHost.cs +++ b/Robust.Server/Console/ServerConsoleHost.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -11,7 +10,6 @@ using Robust.Shared.Network; using Robust.Shared.Network.Messages; using Robust.Shared.Player; using Robust.Shared.Toolshed; -using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Utility; namespace Robust.Server.Console @@ -162,8 +160,8 @@ namespace Robust.Server.Console { var message = new MsgConCmdReg(); - var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands().ToArray(); - message.Commands = new List(AvailableCommands.Count + toolshedCommands.Length); + var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands(); + message.Commands = new List(AvailableCommands.Count + toolshedCommands.Count); var commands = new HashSet(); foreach (var command in AvailableCommands.Values) @@ -240,20 +238,15 @@ namespace Robust.Server.Console if ((result == null) || message.Args.Length <= 1) { - var parser = new ParserContext(message.ArgString, _toolshed); - CommandRun.TryParse(true, parser, null, null, false, out _, out var completions, out _); - if (completions == null) - { + var shedRes = _toolshed.GetCompletions(shell, message.ArgString); + if (shedRes == null) goto done; - } - var (shedRes, _) = await completions.Value; IEnumerable options = result?.Options ?? Array.Empty(); - if (shedRes != null) - options = options.Concat(shedRes.Options); + options = options.Concat(shedRes.Options); - var hints = result?.Hint ?? shedRes?.Hint; + var hints = result?.Hint ?? shedRes.Hint; result = new CompletionResult(options.ToArray(), hints); } diff --git a/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs b/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs index 6b82b0056..7b5911d85 100644 --- a/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs +++ b/Robust.Server/Toolshed/Commands/Players/PlayersCommand.cs @@ -24,7 +24,7 @@ public sealed class PlayerCommand : ToolshedCommand => _playerManager.Sessions; [CommandImplementation("self")] - public ICommonSession Self([CommandInvocationContext] IInvocationContext ctx) + public ICommonSession Self(IInvocationContext ctx) { if (ctx.Session is null) { @@ -35,10 +35,7 @@ public sealed class PlayerCommand : ToolshedCommand } [CommandImplementation("imm")] - public ICommonSession Immediate( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] string username - ) + public ICommonSession Immediate(IInvocationContext ctx, string username) { _playerManager.TryGetSessionByUsername(username, out var session); @@ -71,7 +68,7 @@ public sealed class PlayerCommand : ToolshedCommand } [CommandImplementation("entity")] - public EntityUid GetPlayerEntity([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] string username) + public EntityUid GetPlayerEntity(IInvocationContext ctx, string username) { return GetPlayerEntity(Immediate(ctx, username)); } diff --git a/Robust.Shared.Scripting/ILCommand.cs b/Robust.Shared.Scripting/ILCommand.cs index 36e9eb70e..4d25cbd7e 100644 --- a/Robust.Shared.Scripting/ILCommand.cs +++ b/Robust.Shared.Scripting/ILCommand.cs @@ -1,5 +1,4 @@ using System.Reflection; -using System.Reflection.Emit; using ILReader; using ILReader.Readers; using Robust.Shared.Toolshed; @@ -11,7 +10,7 @@ public sealed class ILCommand : ToolshedCommand { [CommandImplementation("dumpil")] public void DumpIL( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] MethodInfo info ) { diff --git a/Robust.Shared.Scripting/ScriptGlobalsShared.cs b/Robust.Shared.Scripting/ScriptGlobalsShared.cs index 74807bd40..ff37f7a9a 100644 --- a/Robust.Shared.Scripting/ScriptGlobalsShared.cs +++ b/Robust.Shared.Scripting/ScriptGlobalsShared.cs @@ -282,11 +282,31 @@ namespace Robust.Shared.Scripting return Array.Empty(); } + public bool HasErrors => false; + public void ClearErrors() { } - public Dictionary Variables { get; } = new(); + /// + public object? ReadVar(string name) + { + return Variables.GetValueOrDefault(name); + } + + /// + public void WriteVar(string name, object? value) + { + Variables[name] = value; + } + + /// + public IEnumerable GetVars() + { + return Variables.Keys; + } + + public Dictionary Variables { get; } = new(); private static MemberInfo? ReflectionGetInstanceMember(Type type, MemberTypes memberType, string name) { diff --git a/Robust.Shared/Console/CompletionResult.cs b/Robust.Shared/Console/CompletionResult.cs index 64eab3256..8be56daad 100644 --- a/Robust.Shared/Console/CompletionResult.cs +++ b/Robust.Shared/Console/CompletionResult.cs @@ -17,7 +17,7 @@ public sealed record CompletionResult(CompletionOption[] Options, string? Hint) /// /// Type hint string for the current argument being typed. /// - public string? Hint { get; init; } = Hint; + public string? Hint { get; set; } = Hint; public static readonly CompletionResult Empty = new(Array.Empty(), null); diff --git a/Robust.Shared/GameObjects/Entity.cs b/Robust.Shared/GameObjects/Entity.cs index 9774d8c94..9a8666db7 100644 --- a/Robust.Shared/GameObjects/Entity.cs +++ b/Robust.Shared/GameObjects/Entity.cs @@ -1,9 +1,10 @@ using Robust.Shared.Localization; +using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; namespace Robust.Shared.GameObjects; -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T : IComponent? { public EntityUid Owner; @@ -44,10 +45,12 @@ public record struct Entity : IFluentEntityUid comp = Comp; } + public EntityUid AsType() => Owner; + public override int GetHashCode() => Owner.GetHashCode(); } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? { public EntityUid Owner; @@ -111,9 +114,11 @@ public record struct Entity : IFluentEntityUid { return new Entity(ent.Owner, ent.Comp1); } + + public EntityUid AsType() => Owner; } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? { public EntityUid Owner; @@ -213,9 +218,11 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? { public EntityUid Owner; @@ -339,9 +346,11 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? { public EntityUid Owner; @@ -489,9 +498,11 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? { public EntityUid Owner; @@ -663,9 +674,11 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? { public EntityUid Owner; @@ -861,9 +874,12 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; + } -public record struct Entity : IFluentEntityUid +public record struct Entity : IFluentEntityUid, IAsType where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent? where T5 : IComponent? where T6 : IComponent? where T7 : IComponent? where T8 : IComponent? { public EntityUid Owner; @@ -1083,4 +1099,6 @@ public record struct Entity : IFluentEntityUid } #endregion + + public EntityUid AsType() => Owner; } diff --git a/Robust.Shared/GameObjects/EntityManager.Components.cs b/Robust.Shared/GameObjects/EntityManager.Components.cs index 4a5d30b00..fa3798c22 100644 --- a/Robust.Shared/GameObjects/EntityManager.Components.cs +++ b/Robust.Shared/GameObjects/EntityManager.Components.cs @@ -106,6 +106,7 @@ namespace Robust.Shared.GameObjects /// public int Count(Type component) { + DebugTools.Assert(component.IsAssignableTo(typeof(IComponent))); var dict = _entTraitDict[component]; return dict.Count; } @@ -1125,6 +1126,78 @@ namespace Robust.Shared.GameObjects i++; } + // Count includes "deleted" components that are not returned by MoveNext() + // This ensures that we dont return an array with empty/invalid entries + Array.Resize(ref comps, i); + return comps; + } + + public Entity[] AllEntities() where T : IComponent + { + var query = AllEntityQueryEnumerator(); + var comps = new Entity[Count()]; + var i = 0; + + while (query.MoveNext(out var uid, out var comp)) + { + comps[i++] = (uid, comp); + } + + // Count includes "deleted" components that are not returned by MoveNext() + // This ensures that we dont return an array with empty/invalid entries + Array.Resize(ref comps, i); + return comps; + } + + public Entity[] AllEntities(Type tComp) + { + var query = AllEntityQueryEnumerator(tComp); + var comps = new Entity[Count(tComp)]; + var i = 0; + + while (query.MoveNext(out var uid, out var comp)) + { + comps[i++] = (uid, comp); + } + + // Count() includes "deleted" components that are not returned by MoveNext() + // This ensures that we dont return an array with empty/invalid entries + Array.Resize(ref comps, i); + return comps; + } + + + public EntityUid[] AllEntityUids() where T : IComponent + { + var query = AllEntityQueryEnumerator(); + var comps = new EntityUid[Count()]; + var i = 0; + + while (query.MoveNext(out var uid, out _)) + { + comps[i++] = uid; + } + + // Count includes "deleted" components that are not returned by MoveNext() + // This ensures that we dont return an array with empty/invalid entries + Array.Resize(ref comps, i); + return comps; + } + + public EntityUid[] AllEntityUids(Type tComp) + { + var query = AllEntityQueryEnumerator(tComp); + var comps = new EntityUid[Count(tComp)]; + var i = 0; + + while (query.MoveNext(out var uid, out _)) + { + comps[i++] = uid; + } + + // Count() includes "deleted" components that are not returned by MoveNext() + // This ensures that we dont return an array with empty/invalid entries + Array.Resize(ref comps, i); return comps; } @@ -1169,6 +1242,13 @@ namespace Robust.Shared.GameObjects return new CompRegistryEntityEnumerator(this, trait1, registry); } + public AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp) + { + DebugTools.Assert(comp.IsAssignableTo(typeof(IComponent))); + var trait = _entTraitArray[_componentFactory.GetIndex(comp).Value]; + return new AllEntityQueryEnumerator(trait); + } + public AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent { diff --git a/Robust.Shared/GameObjects/EntityUid.cs b/Robust.Shared/GameObjects/EntityUid.cs index 3974fb4ee..989c6a0da 100644 --- a/Robust.Shared/GameObjects/EntityUid.cs +++ b/Robust.Shared/GameObjects/EntityUid.cs @@ -47,16 +47,14 @@ namespace Robust.Shared.GameObjects public static bool TryParse(ReadOnlySpan uid, out EntityUid entityUid) { - try + if (!int.TryParse(uid, out var id)) { - entityUid = Parse(uid); - return true; - } - catch (FormatException) - { - entityUid = Invalid; + entityUid = default; return false; } + + entityUid = new(id); + return true; } /// diff --git a/Robust.Shared/GameObjects/IEntityManager.Components.cs b/Robust.Shared/GameObjects/IEntityManager.Components.cs index e0e7a83b1..84b47c53f 100644 --- a/Robust.Shared/GameObjects/IEntityManager.Components.cs +++ b/Robust.Shared/GameObjects/IEntityManager.Components.cs @@ -410,6 +410,30 @@ namespace Robust.Shared.GameObjects /// (EntityUid Uid, T Component)[] AllComponents() where T : IComponent; + /// + /// Returns an array of all entities that have the given component. + /// Use sparingly. + /// + Entity[] AllEntities() where T : IComponent; + + /// + /// Returns an array of all entities that have the given component. + /// Use sparingly. + /// + Entity[] AllEntities(Type tComp); + + /// + /// Returns an array uids of all entities that have the given component. + /// Use sparingly. + /// + EntityUid[] AllEntityUids() where T : IComponent; + + /// + /// Returns an array uids of all entities that have the given component. + /// Use sparingly. + /// + EntityUid[] AllEntityUids(Type tComp); + /// /// Returns all instances of a component in a List. /// Use sparingly. @@ -426,6 +450,8 @@ namespace Robust.Shared.GameObjects /// public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry); + AllEntityQueryEnumerator AllEntityQueryEnumerator(Type comp); + AllEntityQueryEnumerator AllEntityQueryEnumerator() where TComp1 : IComponent; diff --git a/Robust.Shared/GameObjects/NetEntity.cs b/Robust.Shared/GameObjects/NetEntity.cs index ffe6ff522..e65dedf91 100644 --- a/Robust.Shared/GameObjects/NetEntity.cs +++ b/Robust.Shared/GameObjects/NetEntity.cs @@ -51,28 +51,42 @@ public readonly struct NetEntity : IEquatable, IComparable if (uid.Length == 0) throw new FormatException($"An empty string is not a valid NetEntity"); + // 'c' prefix for client-side entities if (uid[0] != 'c') return new NetEntity(int.Parse(uid)); if (uid.Length == 1) throw new FormatException($"'c' is not a valid NetEntity"); - var id = int.Parse(uid.Slice(1)); + var id = int.Parse(uid[1..]); return new NetEntity(id | ClientEntity); } public static bool TryParse(ReadOnlySpan uid, out NetEntity entity) { - try + entity = Invalid; + int id; + if (uid.Length == 0) + return false; + + // 'c' prefix for client-side entities + if (uid[0] != 'c') { - entity = Parse(uid); + if (!int.TryParse(uid, out id)) + return false; + + entity = new NetEntity(id); return true; } - catch (Exception ex) when (ex is FormatException or OverflowException) - { - entity = Invalid; + + if (uid.Length == 1) return false; - } + + if (!int.TryParse(uid[1..], out id)) + return false; + + entity = new NetEntity(id | ClientEntity); + return true; } /// diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs index 3a2c4fa28..634c17741 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.ComponentQueries.cs @@ -13,7 +13,6 @@ using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Shapes; using Robust.Shared.Physics.Systems; using Robust.Shared.Utility; -using TerraFX.Interop.Windows; namespace Robust.Shared.GameObjects; diff --git a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs index 2462af5db..9d49df23b 100644 --- a/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs +++ b/Robust.Shared/GameObjects/Systems/EntityLookupSystem.cs @@ -4,22 +4,18 @@ using System.Diagnostics.CodeAnalysis; using System.Numerics; using Robust.Shared.Containers; using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Map; using Robust.Shared.Map.Components; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Physics; -using Robust.Shared.Physics.BroadPhase; using Robust.Shared.Physics.Collision; -using Robust.Shared.Physics.Collision.Shapes; using Robust.Shared.Physics.Components; using Robust.Shared.Physics.Dynamics; using Robust.Shared.Physics.Events; using Robust.Shared.Physics.Systems; using Robust.Shared.Timing; using Robust.Shared.Utility; -using TerraFX.Interop.Windows; namespace Robust.Shared.GameObjects; diff --git a/Robust.Shared/Prototypes/EntProtoId.cs b/Robust.Shared/Prototypes/EntProtoId.cs index c98596b92..f0ba69dd2 100644 --- a/Robust.Shared/Prototypes/EntProtoId.cs +++ b/Robust.Shared/Prototypes/EntProtoId.cs @@ -4,6 +4,7 @@ using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Serialization; using Robust.Shared.Serialization.TypeSerializers.Implementations; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Prototypes; @@ -16,7 +17,7 @@ namespace Robust.Shared.Prototypes; /// /// for a wrapper of other prototype kinds. [Serializable, NetSerializable] -public readonly record struct EntProtoId(string Id) : IEquatable, IComparable +public readonly record struct EntProtoId(string Id) : IEquatable, IComparable, IAsType { public static implicit operator string(EntProtoId protoId) { @@ -48,6 +49,8 @@ public readonly record struct EntProtoId(string Id) : IEquatable, ICompa return string.Compare(Id, other.Id, StringComparison.Ordinal); } + public string AsType() => Id; + public override string ToString() => Id ?? string.Empty; } diff --git a/Robust.Shared/Prototypes/ProtoId.cs b/Robust.Shared/Prototypes/ProtoId.cs index 18ff7e2ca..8d69ad02b 100644 --- a/Robust.Shared/Prototypes/ProtoId.cs +++ b/Robust.Shared/Prototypes/ProtoId.cs @@ -1,5 +1,6 @@ using System; using Robust.Shared.Serialization.TypeSerializers.Implementations.Generic; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Prototypes; @@ -14,7 +15,11 @@ namespace Robust.Shared.Prototypes; /// for an alias. [Serializable] [PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))] -public readonly record struct ProtoId(string Id) : IEquatable, IComparable> where T : class, IPrototype +public readonly record struct ProtoId(string Id) : + IEquatable, + IComparable>, + IAsType + where T : class, IPrototype { public static implicit operator string(ProtoId protoId) { @@ -46,5 +51,7 @@ public readonly record struct ProtoId(string Id) : IEquatable, ICompa return string.Compare(Id, other.Id, StringComparison.Ordinal); } + public string AsType() => Id; + public override string ToString() => Id ?? string.Empty; } diff --git a/Robust.Shared/Toolshed/Attributes.cs b/Robust.Shared/Toolshed/Attributes.cs index 1300fd38d..8fcb57bbc 100644 --- a/Robust.Shared/Toolshed/Attributes.cs +++ b/Robust.Shared/Toolshed/Attributes.cs @@ -1,5 +1,7 @@ using System; using JetBrains.Annotations; +using Robust.Shared.Toolshed.TypeParsers; +using Robust.Shared.Utility; namespace Robust.Shared.Toolshed; @@ -33,18 +35,28 @@ public sealed class CommandImplementationAttribute : Attribute /// [AttributeUsage(AttributeTargets.Parameter)] [MeansImplicitUse] -public sealed class PipedArgumentAttribute : Attribute -{ -} +public sealed class PipedArgumentAttribute : Attribute; /// -/// Marks an argument in a function as being an argument of a . -/// This will make it so the argument will get parsed. +/// Marks an argument in a function as being an argument of a . Unless a custom parser is +/// specified, the default parser for the argument's type will be used. This attribute is implicitly present if a +/// parameter has no other relevant attributes and the parameter type is not . /// [AttributeUsage(AttributeTargets.Parameter)] [MeansImplicitUse] public sealed class CommandArgumentAttribute : Attribute { + public CommandArgumentAttribute(Type? customParser = null) + { + if (customParser == null) + return; + + CustomParser = customParser; + DebugTools.Assert(customParser.IsCustomParser(), + $"Custom parser {customParser.PrettyName()} does not inherit from {typeof(CustomTypeParser<>).PrettyName()}"); + } + + public Type? CustomParser { get; } } /// @@ -52,19 +64,17 @@ public sealed class CommandArgumentAttribute : Attribute /// [AttributeUsage(AttributeTargets.Parameter)] [MeansImplicitUse] -public sealed class CommandInvertedAttribute : Attribute -{ -} +public sealed class CommandInvertedAttribute : Attribute; /// -/// Marks an argument in a function as being where the invocation context should be provided in a . +/// Marks an argument in a function as being where the invocation context should be provided in a +/// . This attribute is implicitly present if one of the arguments is of type +/// and has no other relevant attributes. /// /// [AttributeUsage(AttributeTargets.Parameter)] [MeansImplicitUse] -public sealed class CommandInvocationContextAttribute : Attribute -{ -} +public sealed class CommandInvocationContextAttribute : Attribute; /// /// Marks a command implementation as taking the type of the previous command in sequence as a generic argument. Supports only one generic type. @@ -74,18 +84,4 @@ public sealed class CommandInvocationContextAttribute : Attribute /// Toolshed will account for this by using . It's not very precise. /// [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; - } -} +public sealed class TakesPipedTypeAsGenericAttribute : Attribute; diff --git a/Robust.Shared/Toolshed/Commands/Entities/Components/CompCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/Components/CompCommand.cs index 7c69e6703..16982ffe0 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/Components/CompCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/Components/CompCommand.cs @@ -9,7 +9,8 @@ namespace Robust.Shared.Toolshed.Commands.Entities.Components; [ToolshedCommand] internal sealed class CompCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(ComponentType)}; + private static Type[] _parsers = [typeof(ComponentTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation("get")] public IEnumerable CompEnumerable([PipedArgument] IEnumerable input) diff --git a/Robust.Shared/Toolshed/Commands/Entities/DeleteCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/DeleteCommand.cs index 874c125ed..2613202ea 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/DeleteCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/DeleteCommand.cs @@ -16,15 +16,8 @@ internal sealed class DeleteCommand : ToolshedCommand } [CommandImplementation] - public void Delete([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] int entityInt) + public void Delete(EntityUid entity) { - if (!EntityManager.TryGetEntity(new NetEntity(entityInt), out var entity) || - !EntityManager.EntityExists(entity)) - { - ctx.WriteLine("That entity does not exist."); - return; - } - - Del(entity.Value); + Del(entity); } } diff --git a/Robust.Shared/Toolshed/Commands/Entities/DoCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/DoCommand.cs index dde19d7ad..f0a986c7b 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/DoCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/DoCommand.cs @@ -13,9 +13,9 @@ internal sealed class DoCommand : ToolshedCommand [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable Do( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] string command) + string command) { if (ctx is not OldShellInvocationContext { } reqCtx || reqCtx.Shell == null) { diff --git a/Robust.Shared/Toolshed/Commands/Entities/EntitiesCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/EntitiesCommand.cs index 24aee9dd8..d813ee4c8 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/EntitiesCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/EntitiesCommand.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Robust.Shared.GameObjects; @@ -10,7 +11,26 @@ internal sealed class EntitiesCommand : ToolshedCommand [CommandImplementation] public IEnumerable Entities() { - // NOTE: Makes a copy due to the fact chained on commands might modify this list. - return EntityManager.GetEntities().ToHashSet(); + return new AllEntityEnumerator(EntityManager); + } + + public sealed class AllEntityEnumerator(IEntityManager entMan) : IEnumerable + { + public IEntityManager EntMan { get; } = entMan; + + // We create an array as chained commands might modify it. + public EntityUid[]? _arr; + + public IEnumerator GetEnumerator() + { + _arr ??= EntMan.GetEntities().ToArray(); + return ((IEnumerable)_arr).GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + _arr ??= EntMan.GetEntities().ToArray(); + return _arr.GetEnumerator(); + } } } diff --git a/Robust.Shared/Toolshed/Commands/Entities/NamedCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/NamedCommand.cs index 4773d415c..0f34c8b9a 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/NamedCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/NamedCommand.cs @@ -9,7 +9,7 @@ namespace Robust.Shared.Toolshed.Commands.Entities; internal sealed class NamedCommand : ToolshedCommand { [CommandImplementation] - public IEnumerable Named([PipedArgument] IEnumerable input, [CommandArgument] string regex, [CommandInverted] bool inverted) + public IEnumerable Named([PipedArgument] IEnumerable input, string regex, [CommandInverted] bool inverted) { var compiled = new Regex($"^{regex}$"); return input.Where(x => compiled.IsMatch(EntName(x)) ^ inverted); diff --git a/Robust.Shared/Toolshed/Commands/Entities/NearbyCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/NearbyCommand.cs index 4c8aa4e32..19269bc63 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/NearbyCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/NearbyCommand.cs @@ -15,11 +15,7 @@ internal sealed class NearbyCommand : ToolshedCommand private EntityLookupSystem? _lookup; [CommandImplementation] - public IEnumerable Nearby( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable input, - [CommandArgument] float range - ) + public IEnumerable Nearby([PipedArgument] IEnumerable input, float range) { var rangeLimit = _cfg.GetCVar(CVars.ToolshedNearbyLimit); if (range > rangeLimit) diff --git a/Robust.Shared/Toolshed/Commands/Entities/PrototypedCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/PrototypedCommand.cs index bf26f3b7e..68197a45d 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/PrototypedCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/PrototypedCommand.cs @@ -2,7 +2,6 @@ using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; -using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Entities; @@ -12,9 +11,9 @@ internal sealed class PrototypedCommand : ToolshedCommand [CommandImplementation] public IEnumerable Prototyped( [PipedArgument] IEnumerable input, - [CommandArgument] Prototype prototype, + EntProtoId prototype, [CommandInverted] bool inverted ) - => input.Where(x => MetaData(x).EntityPrototype?.ID == prototype.Value.ID ^ inverted); + => input.Where(x => MetaData(x).EntityPrototype?.ID == prototype.Id ^ inverted); } diff --git a/Robust.Shared/Toolshed/Commands/Entities/ReplaceCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/ReplaceCommand.cs index 53e94009f..bfa3f3dc0 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/ReplaceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/ReplaceCommand.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Robust.Shared.GameObjects; using Robust.Shared.Prototypes; -using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Entities; @@ -9,10 +8,7 @@ namespace Robust.Shared.Toolshed.Commands.Entities; internal sealed class ReplaceCommand : ToolshedCommand { [CommandImplementation] - public IEnumerable Replace( - [PipedArgument] IEnumerable input, - [CommandArgument] Prototype prototype - ) + public IEnumerable Replace([PipedArgument] IEnumerable input, EntProtoId prototype) { foreach (var i in input) { @@ -20,7 +16,7 @@ internal sealed class ReplaceCommand : ToolshedCommand var coords = xform.Coordinates; var rot = xform.LocalRotation; QDel(i); // yeet - var res = Spawn(prototype.Value.ID, coords); + var res = Spawn(prototype, coords); Transform(res).LocalRotation = rot; yield return res; } diff --git a/Robust.Shared/Toolshed/Commands/Entities/SpawnCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/SpawnCommand.cs index 5d93da201..62ec386d1 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/SpawnCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/SpawnCommand.cs @@ -4,8 +4,6 @@ using System.Numerics; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Prototypes; -using Robust.Shared.Toolshed.Syntax; -using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Entities; @@ -14,61 +12,37 @@ internal sealed class SpawnCommand : ToolshedCommand { #region spawn:at implementations [CommandImplementation("at")] - public EntityUid SpawnAt( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityCoordinates target, - [CommandArgument] ValueRef> proto - ) + public EntityUid SpawnAt([PipedArgument] EntityCoordinates target, EntProtoId proto) { - return Spawn(proto.Evaluate(ctx), target); + return Spawn(proto, target); } [CommandImplementation("at")] - public IEnumerable SpawnAt( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable target, - [CommandArgument] ValueRef> proto - ) - => target.Select(x => SpawnAt(ctx, x, proto)); + public IEnumerable SpawnAt([PipedArgument] IEnumerable target, EntProtoId proto) + => target.Select(x => SpawnAt(x, proto)); #endregion #region spawn:on implementations [CommandImplementation("on")] - public EntityUid SpawnOn( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid target, - [CommandArgument] ValueRef> proto - ) + public EntityUid SpawnOn([PipedArgument] EntityUid target, EntProtoId proto) { - return Spawn(proto.Evaluate(ctx), Transform(target).Coordinates); + return Spawn(proto, Transform(target).Coordinates); } [CommandImplementation("on")] - public IEnumerable SpawnOn( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable target, - [CommandArgument] ValueRef> proto - ) - => target.Select(x => SpawnOn(ctx, x, proto)); + public IEnumerable SpawnOn([PipedArgument] IEnumerable target, EntProtoId proto) + => target.Select(x => SpawnOn(x, proto)); #endregion #region spawn:attached implementations [CommandImplementation("attached")] - public EntityUid SpawnIn( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid target, - [CommandArgument] ValueRef> proto - ) + public EntityUid SpawnIn([PipedArgument] EntityUid target, EntProtoId proto) { - return Spawn(proto.Evaluate(ctx), new EntityCoordinates(target, Vector2.Zero)); + return Spawn(proto, new EntityCoordinates(target, Vector2.Zero)); } [CommandImplementation("attached")] - public IEnumerable SpawnIn( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable target, - [CommandArgument] ValueRef> proto - ) - => target.Select(x => SpawnIn(ctx, x, proto)); + public IEnumerable SpawnIn([PipedArgument] IEnumerable target, EntProtoId proto) + => target.Select(x => SpawnIn(x, proto)); #endregion } diff --git a/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs index a0feadaaa..17f84745c 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/WithCommand.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -15,28 +16,34 @@ internal sealed class WithCommand : ToolshedCommand [CommandImplementation] public IEnumerable With( [PipedArgument] IEnumerable input, - [CommandArgument] ComponentType ty, + [CommandArgument(typeof(ComponentTypeParser))] Type component, [CommandInverted] bool inverted ) { - return input.Where(x => EntityManager.HasComponent(x, ty.Ty) ^ inverted); + if (inverted) + return input.Where(x => !EntityManager.HasComponent(x, component)); + + if (input is EntitiesCommand.AllEntityEnumerator) + return EntityManager.AllEntityUids(component); + + return input.Where(x => EntityManager.HasComponent(x, component)); } [CommandImplementation] public IEnumerable With( [PipedArgument] IEnumerable input, - [CommandArgument] ComponentType ty, + [CommandArgument(typeof(ComponentTypeParser))] Type component, [CommandInverted] bool inverted ) { - var name = _componentFactory.GetComponentName(ty.Ty); + var name = _componentFactory.GetComponentName(component); return input.Where(x => x.Components.ContainsKey(name) ^ inverted); } [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable> With( [PipedArgument] IEnumerable> input, - [CommandArgument] ProtoId protoId, + ProtoId protoId, [CommandInverted] bool inverted ) where T : class, IPrototype { diff --git a/Robust.Shared/Toolshed/Commands/Entities/World/PosCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/World/PosCommand.cs index b62c3f2db..20725d5c4 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/World/PosCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/World/PosCommand.cs @@ -16,7 +16,7 @@ internal sealed class PosCommand : ToolshedCommand => input.Select(Pos); [CommandImplementation] - public EntityCoordinates Pos([CommandInvocationContext] IInvocationContext ctx) + public EntityCoordinates Pos(IInvocationContext ctx) { if (ExecutingEntity(ctx) is { } ent) return Transform(ent).Coordinates; diff --git a/Robust.Shared/Toolshed/Commands/Entities/World/TpCommand.cs b/Robust.Shared/Toolshed/Commands/Entities/World/TpCommand.cs index c8cfa38c1..07a8c52ac 100644 --- a/Robust.Shared/Toolshed/Commands/Entities/World/TpCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Entities/World/TpCommand.cs @@ -2,9 +2,7 @@ using System.Linq; using System.Numerics; using Robust.Shared.GameObjects; -using Robust.Shared.IoC; using Robust.Shared.Map; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Entities.World; @@ -14,62 +12,38 @@ internal sealed class TpCommand : ToolshedCommand private SharedTransformSystem? _xform; [CommandImplementation("coords")] - public EntityUid TpCoords( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid teleporter, - [CommandArgument] ValueRef target - ) + public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target) { _xform ??= GetSys(); - _xform.SetCoordinates(teleporter, target.Evaluate(ctx)); + _xform.SetCoordinates(teleporter, target); return teleporter; } [CommandImplementation("coords")] - public IEnumerable TpCoords( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable teleporters, - [CommandArgument] ValueRef target - ) - => teleporters.Select(x => TpCoords(ctx, x, target)); + public IEnumerable TpCoords([PipedArgument] IEnumerable teleporters, EntityCoordinates target) + => teleporters.Select(x => TpCoords(x, target)); [CommandImplementation("to")] - public EntityUid TpTo( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid teleporter, - [CommandArgument] ValueRef target - ) + public EntityUid TpTo([PipedArgument] EntityUid teleporter, EntityUid target) { _xform ??= GetSys(); - _xform.SetCoordinates(teleporter, Transform(target.Evaluate(ctx)).Coordinates); + _xform.SetCoordinates(teleporter, Transform(target).Coordinates); return teleporter; } [CommandImplementation("to")] - public IEnumerable TpTo( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable teleporters, - [CommandArgument] ValueRef target - ) - => teleporters.Select(x => TpTo(ctx, x, target)); + public IEnumerable TpTo([PipedArgument] IEnumerable teleporters, EntityUid target) + => teleporters.Select(x => TpTo(x, target)); [CommandImplementation("into")] - public EntityUid TpInto( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] EntityUid teleporter, - [CommandArgument] ValueRef target - ) + public EntityUid TpInto([PipedArgument] EntityUid teleporter, EntityUid target) { _xform ??= GetSys(); - _xform.SetCoordinates(teleporter, new EntityCoordinates(target.Evaluate(ctx), Vector2.Zero)); + _xform.SetCoordinates(teleporter, new EntityCoordinates(target, Vector2.Zero)); return teleporter; } [CommandImplementation("into")] - public IEnumerable TpInto( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable teleporters, - [CommandArgument] ValueRef target - ) - => teleporters.Select(x => TpInto(ctx, x, target)); + public IEnumerable TpInto([PipedArgument] IEnumerable teleporters, EntityUid target) + => teleporters.Select(x => TpInto(x, target)); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/AsCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/AsCommand.cs index 28f6aeb48..d740298db 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/AsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/AsCommand.cs @@ -1,11 +1,13 @@ using System; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic; [ToolshedCommand] public sealed class AsCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => [ typeof(Type) ]; + private static Type[] _parsers = [typeof(TypeTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; /// /// Uses a typecast to convert a type. It does not handle implicit casts, nor explicit ones. diff --git a/Robust.Shared/Toolshed/Commands/Generic/ContainsCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/ContainsCommand.cs new file mode 100644 index 000000000..5194bb7a3 --- /dev/null +++ b/Robust.Shared/Toolshed/Commands/Generic/ContainsCommand.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Robust.Shared.Toolshed.Commands.Generic; + +[ToolshedCommand] +internal sealed class ContainsCommand : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public bool Contains([PipedArgument] IEnumerable input, T value, [CommandInverted] bool inverted) + { + return inverted ^ input.Contains(value); + } +} diff --git a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs index e330846e6..8cf76e377 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/EmplaceCommand.cs @@ -1,175 +1,255 @@ using System; using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic; -[ToolshedCommand, MapLikeCommand(false)] +[ToolshedCommand] public sealed class EmplaceCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(EmplaceBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] TOut Emplace( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] TIn value, - [CommandArgument] Block block + [CommandArgument(typeof(EmplaceBlockParser))] Block block ) { - var emplaceCtx = new EmplaceContext(ctx, value, EntityManager); - return block.Invoke(null, emplaceCtx)!; + var emplaceCtx = new EmplaceContext(ctx, EntityManager); + emplaceCtx.Value = value; + return (TOut) (block.Invoke(null, emplaceCtx)!); } [CommandImplementation, TakesPipedTypeAsGeneric] IEnumerable Emplace( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable value, - [CommandArgument] Block block + [CommandArgument(typeof(EmplaceBlockParser))] Block block ) { - + var emplaceCtx = new EmplaceContext(ctx, EntityManager); foreach (var v in value) { - var emplaceCtx = new EmplaceContext(ctx, v, EntityManager); - yield return block.Invoke(null, emplaceCtx)!; + if (ctx.HasErrors) + yield break; + + emplaceCtx.Value = v; + yield return (TOut) (block.Invoke(null, emplaceCtx)!); } } -} -internal record EmplaceContext(IInvocationContext Inner, T Value, IEntityManager EntityManager) : IInvocationContext -{ - public bool CheckInvokable(CommandSpec command, out IConError? error) + private record EmplaceContext : IInvocationContext { - return Inner.CheckInvokable(command, out error); - } - - public ICommonSession? Session => Inner.Session; - public ToolshedManager Toolshed => Inner.Toolshed; - public NetUserId? User => Inner.User; - - public ToolshedEnvironment Environment => Inner.Environment; - - public void WriteLine(string line) - { - Inner.WriteLine(line); - } - - public void ReportError(IConError err) - { - Inner.ReportError(err); - } - - public IEnumerable GetErrors() - { - return Inner.GetErrors(); - } - - public void ClearErrors() - { - Inner.ClearErrors(); - } - - public Dictionary Variables => default!; // we never use this. - - public IEnumerable GetVars() - { - // note: this lies. - return Inner.GetVars(); - } - - public object? ReadVar(string name) - { - if (name == "value") - return Value; - - if (Value is IEmplaceBreakout breakout) + public EmplaceContext(IInvocationContext inner, IEntityManager entMan, T? value = default) { - if (breakout.TryReadVar(name, out var value)) - return value; - } + _inner = inner; + _entMan = entMan; + Value = value; - if (Value is EntityUid id) - { - switch (name) + _localVars.Add("value"); + if (typeof(T) == typeof(EntityUid)) { - case "wy": - case "wx": - { - var xform = EntityManager.GetComponent(id); - var sys = EntityManager.System(); - 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(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(); - } + _localVars.Add("wx"); + _localVars.Add("wy"); + _localVars.Add("proto"); + _localVars.Add("name"); + _localVars.Add("desc"); + _localVars.Add("paused"); } - } - else if (Value is ICommonSession session) - { - switch (name) + else if (typeof(T).IsAssignableTo(typeof(ICommonSession))) { - case "ent": - { - return EntityManager.GetNetEntity(session.AttachedEntity!); - } - case "name": - { - return session.Name; - } - case "userid": - { - return session.UserId; - } + _localVars.Add("ent"); + _localVars.Add("name"); + _localVars.Add("userid"); } } - return Inner.ReadVar(name); - } + public T? Value; + private readonly IInvocationContext _inner; + private readonly IEntityManager _entMan; + private readonly HashSet _localVars = new(); - public void WriteVar(string name, object? value) - { - if (name == "value") - return; - - if (Value is IEmplaceBreakout v) + public bool CheckInvokable(CommandSpec command, out IConError? error) { - if (v.VarsOverriden.Contains(name)) - return; + return _inner.CheckInvokable(command, out error); } - Inner.WriteVar(name, value); + public ICommonSession? Session => _inner.Session; + public ToolshedManager Toolshed => _inner.Toolshed; + public NetUserId? User => _inner.User; + + public ToolshedEnvironment Environment => _inner.Environment; + + public void WriteLine(string line) + { + _inner.WriteLine(line); + } + + public void ReportError(IConError err) + { + _inner.ReportError(err); + } + + public IEnumerable GetErrors() + { + return _inner.GetErrors(); + } + + public bool HasErrors => _inner.HasErrors; + + public void ClearErrors() + { + _inner.ClearErrors(); + } + + + public IEnumerable GetVars() + { + foreach (var name in _localVars) + { + yield return name; + } + + foreach (var inner in _inner.GetVars()) + { + if (!_localVars.Contains(inner)) + yield return inner; + } + } + + public object? ReadVar(string name) + { + if (name == "value") + return Value; + + return Value switch + { + EntityUid uid => name switch + { + "wx" => _entMan.System().GetWorldPosition(uid).X, + "wy" => _entMan.System().GetWorldPosition(uid).Y, + "proto" => _entMan.GetComponent(uid).EntityPrototype?.ID ?? "", + "desc" => _entMan.GetComponent(uid).EntityDescription, + "name" => _entMan.GetComponent(uid).EntityName, + "paused" => _entMan.GetComponent(uid).EntityPaused, + _ => _inner.ReadVar(name) + }, + ICommonSession session => name switch + { + "ent" => session.AttachedEntity!, + "name" => session.Name, + "userid" => session.UserId, + _ => _inner.ReadVar(name) + }, + _ => _inner.ReadVar(name) + }; + } + + public void WriteVar(string name, object? value) + { + if (_localVars.Contains(name)) + ReportError(new ReadonlyVariableError(name)); + else + _inner.WriteVar(name, value); + } + + public bool IsReadonlyVar(string name) => _localVars.Contains(name); + } + + /// + /// Custom block parser for the is aware of the variables defined within the + /// . + /// + private sealed class EmplaceBlockParser : CustomTypeParser + { + public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result) + { + // If the piped type is IEnumerable we want to extract the type T. + var pipeInferredType = ctx.Bundle.PipedType!; + if (pipeInferredType.IsGenericType(typeof(IEnumerable<>))) + pipeInferredType = pipeInferredType.GetGenericArguments()[0]; + + var localParser = SetupVarParser(ctx, pipeInferredType); + var success = Block.TryParseBlock(ctx, null, null, out result); + ctx.VariableParser = localParser.Inner; + return success; + } + + private static LocalVarParser SetupVarParser(ParserContext ctx, Type input) + { + var localParser = new LocalVarParser(ctx.VariableParser); + ctx.VariableParser = localParser; + localParser.SetLocalType("value", input, true); + if (input == typeof(EntityUid)) + { + localParser.SetLocalType("wx", typeof(float), true); + localParser.SetLocalType("wy", typeof(float), true); + localParser.SetLocalType("proto", typeof(string), true); + localParser.SetLocalType("desc", typeof(string), true); + localParser.SetLocalType("name", typeof(string), true); + localParser.SetLocalType("paused", typeof(bool), true); + } + else if (input.IsAssignableTo(typeof(ICommonSession))) + { + localParser.SetLocalType("ent", typeof(EntityUid), true); + localParser.SetLocalType("name", typeof(string), true); + localParser.SetLocalType("userid", typeof(NetUserId), true); + } + + return localParser; + } + + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? result) + { + result = null; + if (!TryParse(ctx, out var run)) + return false; + + result = new Block(run); + return true; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + TryParse(ctx, out _); + return ctx.Completions; + } + } + + /// + /// This custom parser is for parsing the type returned by the block used in the an . + /// + private sealed class EmplaceBlockOutputParser : CustomTypeParser + { + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) + { + result = null; + var save = ctx.Save(); + if (!EmplaceBlockParser.TryParse(ctx, out var block)) + return false; + + if (block.ReturnType == null) + return false; + + ctx.Restore(save); + result = block.ReturnType; + return true; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + EmplaceBlockParser.TryParse(ctx, out _); + return ctx.Completions; + } } } - -public interface IEmplaceBreakout -{ - public ImmutableHashSet VarsOverriden { get; } - public bool TryReadVar(string name, out object? value); -} diff --git a/Robust.Shared/Toolshed/Commands/Generic/IterateCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/IterateCommand.cs index 44fa02892..381327ccd 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/IterateCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/IterateCommand.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Collections.Generic; using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic; @@ -10,18 +8,20 @@ public sealed class IterateCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable? Iterate( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] T value, - [CommandArgument] Block block, - [CommandArgument] ValueRef times + Block block, + int times ) { - var iCap = times.Evaluate(ctx); - - for (var i = 0; i < iCap; i++) + for (var i = 0; i < times; i++) { if (block.Invoke(value, ctx) is not { } v) break; + + if (ctx.HasErrors) + break; + value = v; yield return value; } diff --git a/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/RepeatCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/RepeatCommand.cs index 98e5905aa..9aa626356 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/RepeatCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/RepeatCommand.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic.ListGeneration; @@ -8,10 +7,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic.ListGeneration; public sealed class RepeatCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Repeat( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T value, - [CommandArgument] ValueRef amount - ) - => Enumerable.Repeat(value, amount.Evaluate(ctx)); + public IEnumerable Repeat([PipedArgument] T value, int amount) + => Enumerable.Repeat(value, amount); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/ToCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/ToCommand.cs index c5057899a..6c6f785ee 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/ToCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/ListGeneration/ToCommand.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic.ListGeneration; @@ -9,11 +8,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic.ListGeneration; public sealed class ToCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable To( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T start, - [CommandArgument] ValueRef end - ) - where T : INumber - => Enumerable.Range(int.CreateTruncating(start), 1 + int.CreateTruncating(end.Evaluate(ctx)! - start)).Select(T.CreateTruncating); + public IEnumerable To([PipedArgument] T start, T end) where T : INumber + => Enumerable.Range(int.CreateTruncating(start), 1 + int.CreateTruncating(end - start)).Select(T.CreateTruncating); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/MapCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/MapCommand.cs index bd87f7687..62b0d76df 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/MapCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/MapCommand.cs @@ -1,22 +1,30 @@ using System; using System.Collections.Generic; -using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class MapCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable? Map( - [CommandInvocationContext] IInvocationContext ctx, + public IEnumerable Map( + IInvocationContext ctx, [PipedArgument] IEnumerable value, - [CommandArgument] Block block + Block block ) { - return value.Select(x => block.Invoke(x, ctx)).Where(x => x != null).Cast(); + foreach (var x in value) + { + if (block.Invoke(x, ctx) is { } result) + yield return result; + + if (ctx.HasErrors) + break; + } } } diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortByCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortByCommand.cs index bcb2c1e08..0426f3c87 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortByCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortByCommand.cs @@ -2,19 +2,21 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class SortByCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable SortBy( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block orderer + Block orderer ) where TOrd : IComparable => input.OrderBy(x => orderer.Invoke(x, ctx)!); diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortCommand.cs index 972b52e05..551483b07 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortCommand.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; @@ -9,10 +8,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; public sealed class SortCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Sort( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable input - ) - where T : IComparable + public IEnumerable Sort([PipedArgument] IEnumerable input) where T : IComparable => input.Order(); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownByCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownByCommand.cs index 47060d363..386e03c8b 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownByCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownByCommand.cs @@ -2,19 +2,21 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class SortDownByCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable SortBy( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block orderer + Block orderer ) where TOrd : IComparable => input.OrderByDescending(x => orderer.Invoke(x, ctx)!); diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownCommand.cs index e81fefb75..8a38f582a 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortDownCommand.cs @@ -9,10 +9,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; public sealed class SortDownCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Sort( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable input - ) - where T : IComparable + public IEnumerable Sort([PipedArgument] IEnumerable input) where T : IComparable => input.OrderDescending(); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapByCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapByCommand.cs index 05ddc82ba..d255cb163 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapByCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapByCommand.cs @@ -2,19 +2,21 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class SortMapByCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable SortBy( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block orderer + Block orderer ) where TOrd : IComparable => input.Select(x => orderer.Invoke(x, ctx)!).Order(); diff --git a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapDownByCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapDownByCommand.cs index bcbe8907d..ec912ae14 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapDownByCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Ordering/SortMapDownByCommand.cs @@ -2,19 +2,21 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic.Ordering; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class SortMapDownByCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable SortBy( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block orderer + Block orderer ) where TOrd : IComparable => input.Select(x => orderer.Invoke(x, ctx)!).OrderDescending(); diff --git a/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs index 49a02eb84..ec93194e9 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/ReduceCommand.cs @@ -1,9 +1,9 @@ -using System.Collections.Generic; -using System.Linq; -using Robust.Shared.Network; -using Robust.Shared.Player; -using Robust.Shared.Toolshed.Errors; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Console; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic; @@ -12,53 +12,72 @@ public sealed class ReduceCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public T Reduce( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block reducer + [CommandArgument(typeof(ReduceBlockParser))] Block reducer ) - => input.Aggregate((x, next) => reducer.Invoke(x, new ReduceContext(ctx, next))!); -} - -internal record ReduceContext(IInvocationContext Inner, T Value) : IInvocationContext -{ - public bool CheckInvokable(CommandSpec command, out IConError? error) { - return Inner.CheckInvokable(command, out error); + var localCtx = new LocalVarInvocationContext(ctx); + localCtx.SetLocal("value", default(T)); + + using var enumerator = input.GetEnumerator(); + + if (!enumerator.MoveNext()) + throw new InvalidOperationException($"Input contains no elements"); + + var result = enumerator.Current; + + while (enumerator.MoveNext()) + { + localCtx.SetLocal("value", enumerator.Current); + result = (T) reducer.Invoke(result, localCtx)!; + if (ctx.HasErrors) + break; + } + + return result; } - public ICommonSession? Session => Inner.Session; - public ToolshedManager Toolshed => Inner.Toolshed; - public NetUserId? User => Inner.User; - - public ToolshedEnvironment Environment => Inner.Environment; - - public void WriteLine(string line) + /// + /// Custom block parser for the that is aware of the "$value" variable. + /// + private sealed class ReduceBlockParser : CustomTypeParser { - Inner.WriteLine(line); - } + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? result) + { + result = null; + if (ctx.Bundle.PipedType is not {IsGenericType: true}) + return false; - public void ReportError(IConError err) - { - Inner.ReportError(err); - } + var localParser = new LocalVarParser(ctx.VariableParser); + var type = ctx.Bundle.PipedType.GetGenericArguments()[0]; + localParser.SetLocalType("value", type, false); + ctx.VariableParser = localParser; - public IEnumerable GetErrors() - { - return Inner.GetErrors(); - } + if (!Block.TryParseBlock(ctx, type, type, out var run)) + { + result = null; + ctx.VariableParser = localParser.Inner; + return false; + } - public void ClearErrors() - { - Inner.ClearErrors(); - } + ctx.VariableParser = localParser.Inner; + result = new Block(run); + return true; + } - public Dictionary Variables { get; } = new(); + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + if (ctx.Bundle.PipedType is not {IsGenericType: true}) + return null; - public object? ReadVar(string name) - { - if (name == "value") - return Value; - - return Inner.ReadVar(name); + var localParser = new LocalVarParser(ctx.VariableParser); + var type = ctx.Bundle.PipedType.GetGenericArguments()[0]; + localParser.SetLocalType("value", type, false); + ctx.VariableParser = localParser; + Block.TryParseBlock(ctx, type, type, out _); + ctx.VariableParser = localParser.Inner; + return ctx.Completions; + } } } diff --git a/Robust.Shared/Toolshed/Commands/Generic/SelectCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/SelectCommand.cs index 9ece3ebdb..f30df3d4c 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/SelectCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/SelectCommand.cs @@ -12,7 +12,7 @@ public sealed class SelectCommand : ToolshedCommand [Dependency] private readonly IRobustRandom _random = default!; [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Select([PipedArgument] IEnumerable enumerable, [CommandArgument] Quantity quantity, [CommandInverted] bool inverted) + public IEnumerable Select([PipedArgument] IEnumerable enumerable, Quantity quantity, [CommandInverted] bool inverted) { var arr = enumerable.ToArray(); _random.Shuffle(arr); diff --git a/Robust.Shared/Toolshed/Commands/Generic/TakeCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/TakeCommand.cs index f5716b49d..798f86610 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/TakeCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/TakeCommand.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic; @@ -8,10 +7,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic; public sealed class TakeCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Take( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable input, - [CommandArgument] ValueRef amount - ) - => input.Take(amount.Evaluate(ctx)); + public IEnumerable Take([PipedArgument] IEnumerable input, int amount) + => input.Take(amount); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/TeeCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/TeeCommand.cs index 401b9fe78..53e4513b9 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/TeeCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/TeeCommand.cs @@ -1,26 +1,31 @@ using System; using System.Collections.Generic; -using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Generic; -[ToolshedCommand, MapLikeCommand] +[ToolshedCommand] public sealed class TeeCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; + // Take in some input, use it to evaluate some block, and then just keep passing along the input, disregarding the + // output of the block. I.e., this behaves like the standard tee tee command, where the block is the "file". [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Tee( - [CommandInvocationContext] IInvocationContext ctx, + public IEnumerable Tee( + IInvocationContext ctx, [PipedArgument] IEnumerable value, - [CommandArgument] Block block + Block block ) { - return value.Select(x => + foreach (var x in value) { block.Invoke(x, ctx); - return x; - }).Where(x => x != null).Cast(); + if (ctx.HasErrors) + yield break; + yield return x; + } } } diff --git a/Robust.Shared/Toolshed/Commands/Generic/Variables/ArrowCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Variables/ArrowCommand.cs index ebb74b35c..4b361b2f7 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Variables/ArrowCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Variables/ArrowCommand.cs @@ -8,25 +8,17 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Variables ; public sealed class ArrowCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Arrow( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T input, - [CommandArgument] ValueRef @ref - ) + public T Arrow(IInvocationContext ctx, [PipedArgument] T input, WriteableVarRef var) { - @ref.Set(ctx, input); + ctx.WriteVar(var.Inner.VarName, input); return input; } [CommandImplementation, TakesPipedTypeAsGeneric] - public List Arrow( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable input, - [CommandArgument] ValueRef> @ref - ) + public List Arrow(IInvocationContext ctx, [PipedArgument] IEnumerable input, WriteableVarRef> var) { var list = input.ToList(); - @ref.Set(ctx, list); + ctx.WriteVar(var.Inner.VarName, list); return list; } } diff --git a/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs index 3970b1c2b..df5e7efd8 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/Variables/VarsCommand.cs @@ -6,7 +6,7 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Variables; public sealed class VarsCommand : ToolshedCommand { [CommandImplementation] - public void Vars([CommandInvocationContext] IInvocationContext ctx) + public void Vars(IInvocationContext ctx) { ctx.WriteLine(Toolshed.PrettyPrintType(ctx.GetVars().Select(x => $"{x} = {ctx.ReadVar(x)}"), out var more)); } diff --git a/Robust.Shared/Toolshed/Commands/Generic/WhereCommand.cs b/Robust.Shared/Toolshed/Commands/Generic/WhereCommand.cs index 1efd0d069..9e7b7fc75 100644 --- a/Robust.Shared/Toolshed/Commands/Generic/WhereCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Generic/WhereCommand.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Generic; @@ -9,16 +8,16 @@ public sealed class WhereCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable Where( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable input, - [CommandArgument] Block check + Block check ) { foreach (var i in input) { var res = check.Invoke(i, ctx); - if (ctx.GetErrors().Any()) + if (ctx.HasErrors) yield break; if (res) diff --git a/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs b/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs index b4e085741..e55ca5a4a 100644 --- a/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/ArithmeticCommands.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Math; @@ -10,53 +9,33 @@ namespace Robust.Shared.Toolshed.Commands.Math; public sealed class AddCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IAdditionOperators + public T Operation([PipedArgument] T x, T y) where T : IAdditionOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x + yVal; + return x + y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IAdditionOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y) + .Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); [CommandImplementation] - public Vector2 Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] Vector2 x, - [CommandArgument] ValueRef y - ) + public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x + yVal; + return x + y; } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -64,28 +43,16 @@ public sealed class AddCommand : ToolshedCommand public sealed class AddVecCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, T y) where T : IAdditionOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x.Select(i => i + yVal); + return x.Select(i => i + y); } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x.Select(i => i + yVal); + return x.Select(i => i + y); } } @@ -93,53 +60,32 @@ public sealed class AddVecCommand : ToolshedCommand public sealed class SubtractCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : ISubtractionOperators + public T Operation([PipedArgument] T x, T y) where T : ISubtractionOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x - yVal; + return x - y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : ISubtractionOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); [CommandImplementation] - public Vector2 Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] Vector2 x, - [CommandArgument] ValueRef y - ) + public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x - yVal; + return x - y; } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -147,28 +93,16 @@ public sealed class SubtractCommand : ToolshedCommand public sealed class SubVecCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, T y) where T : ISubtractionOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x.Select(i => i - yVal); + return x.Select(i => i - y); } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x.Select(i => i - yVal); + return x.Select(i => i - y); } } @@ -176,51 +110,32 @@ public sealed class SubVecCommand : ToolshedCommand public sealed class MultiplyCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IMultiplyOperators + public T Operation([PipedArgument] T x, T y) where T : IMultiplyOperators { - var yVal = y.Evaluate(ctx)!; - return x * yVal; + return x * y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IMultiplyOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); [CommandImplementation] - public Vector2 Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] Vector2 x, - [CommandArgument] ValueRef y - ) + public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x * yVal; + return x * y; } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -228,28 +143,16 @@ public sealed class MultiplyCommand : ToolshedCommand public sealed class MulVecCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, T y) where T : IMultiplyOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x.Select(i => i * yVal); + return x.Select(i => i * y); } [CommandImplementation] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, Vector2 y) { - var yVal = y.Evaluate(ctx); - return x.Select(i => i * yVal); + return x.Select(i => i * y); } } @@ -257,34 +160,20 @@ public sealed class MulVecCommand : ToolshedCommand public sealed class DivideCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : INumberBase + public T Operation([PipedArgument] T x, T y) where T : INumberBase { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - - if (T.IsZero(yVal)) + if (T.IsZero(y)) return T.Zero; - return x / yVal; + return x / y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : INumberBase - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : INumberBase + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -292,21 +181,12 @@ public sealed class DivideCommand : ToolshedCommand public sealed class DivVecCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) - where T : INumberBase + public IEnumerable Operation([PipedArgument] IEnumerable x, T y) where T : INumberBase { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - - if (T.IsZero(yVal)) + if (T.IsZero(y)) return x.Select(_ => T.Zero); - return x.Select(i => i / yVal); + return x.Select(i => i / y); } } @@ -314,28 +194,18 @@ public sealed class DivVecCommand : ToolshedCommand public sealed class ModulusCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IModulusOperators + public T Operation([PipedArgument] T x, T y) where T : IModulusOperators { - var yVal = y.Evaluate(ctx)!; - return x % yVal; + return x % y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IModulusOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -343,17 +213,9 @@ public sealed class ModulusCommand : ToolshedCommand public sealed class ModVecCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y - ) - where T : IModulusOperators + public IEnumerable Operation([PipedArgument] IEnumerable x, T y) where T : IModulusOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return x.Select(i => i % yVal); + return x.Select(i => i % y); } } #endregion @@ -362,28 +224,17 @@ public sealed class ModVecCommand : ToolshedCommand public sealed class MinCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : INumberBase + public T Operation([PipedArgument] T x, T y) where T : INumberBase { - var yVal = y.Evaluate(ctx)!; - return T.MinMagnitude(x, yVal); + return T.MinMagnitude(x, y); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : INumberBase - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : INumberBase + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -391,28 +242,17 @@ public sealed class MinCommand : ToolshedCommand public sealed class MaxCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : INumberBase + public T Operation([PipedArgument] T x, T y) where T : INumberBase { - var yVal = y.Evaluate(ctx)!; - return T.MaxMagnitude(x, yVal); + return T.MaxMagnitude(x, y); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : INumberBase - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : INumberBase + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -421,28 +261,18 @@ public sealed class MaxCommand : ToolshedCommand public sealed class BitAndCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x & yVal; + return x & y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -450,28 +280,18 @@ public sealed class BitAndCommand : ToolshedCommand public sealed class BitAndNotCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x & ~yVal; + return x & ~y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -479,28 +299,18 @@ public sealed class BitAndNotCommand : ToolshedCommand public sealed class BitOrCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x | yVal; + return x | y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -508,28 +318,18 @@ public sealed class BitOrCommand : ToolshedCommand public sealed class BitOrNotCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x | ~yVal; + return x | ~y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -537,28 +337,18 @@ public sealed class BitOrNotCommand : ToolshedCommand public sealed class BitXorCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x ^ yVal; + return x ^ y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -566,28 +356,18 @@ public sealed class BitXorCommand : ToolshedCommand public sealed class BitXnorCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x, T y) where T : IBitwiseOperators { - var yVal = y.Evaluate(ctx)!; - return x ^ ~yVal; + return x ^ ~y; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IBitwiseOperators - => x.Zip(y.Evaluate(ctx)!).Select(inp => + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -595,22 +375,14 @@ public sealed class BitXnorCommand : ToolshedCommand public sealed class BitNotCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x - ) - where T : IBitwiseOperators + public T Operation([PipedArgument] T x) where T : IBitwiseOperators { return ~x; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x - ) - where T : IBitwiseOperators - => x.Select(v => Operation(ctx, v)); + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : IBitwiseOperators + => x.Select(Operation); } #endregion @@ -619,15 +391,13 @@ public sealed class BitNotCommand : ToolshedCommand public sealed class NegCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation([PipedArgument] T x) - where T : IUnaryNegationOperators + public T Operation([PipedArgument] T x) where T : IUnaryNegationOperators { return -x; } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation([PipedArgument] IEnumerable x) - where T : IUnaryNegationOperators + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : IUnaryNegationOperators => x.Select(Operation); } @@ -635,14 +405,12 @@ public sealed class NegCommand : ToolshedCommand public sealed class AbsCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation([PipedArgument] T x) - where T : INumberBase + public T Operation([PipedArgument] T x) where T : INumberBase { return T.Abs(x); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation([PipedArgument] IEnumerable x) - where T : INumberBase + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : INumberBase => x.Select(Operation); } diff --git a/Robust.Shared/Toolshed/Commands/Math/ComparisonCommands.cs b/Robust.Shared/Toolshed/Commands/Math/ComparisonCommands.cs index ed0610bdc..487c4536c 100644 --- a/Robust.Shared/Toolshed/Commands/Math/ComparisonCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/ComparisonCommands.cs @@ -1,6 +1,5 @@ using System; using System.Numerics; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Math; @@ -8,17 +7,9 @@ namespace Robust.Shared.Toolshed.Commands.Math; public sealed class GreaterThanCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : INumber + public bool Comparison([PipedArgument] T x, T y) where T : INumber { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return x > yVal; + return x > y; } } @@ -26,17 +17,10 @@ public sealed class GreaterThanCommand : ToolshedCommand public sealed class LessThanCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) + public bool Comparison([PipedArgument] T x, T y) where T : IComparisonOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return x > yVal; + return x > y; } } @@ -44,17 +28,10 @@ public sealed class LessThanCommand : ToolshedCommand public sealed class GreaterThanOrEqualCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) + public bool Comparison([PipedArgument] T x, T y) where T : INumber { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return x >= yVal; + return x >= y; } } @@ -62,17 +39,10 @@ public sealed class GreaterThanOrEqualCommand : ToolshedCommand public sealed class LessThanOrEqualCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) + public bool Comparison([PipedArgument] T x, T y) where T : IComparisonOperators { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return x <= yVal; + return x <= y; } } @@ -80,17 +50,10 @@ public sealed class LessThanOrEqualCommand : ToolshedCommand public sealed class EqualCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) + public bool Comparison([PipedArgument] T x, T y) where T : IEquatable { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return x.Equals(yVal); + return x.Equals(y); } } @@ -98,16 +61,9 @@ public sealed class EqualCommand : ToolshedCommand public sealed class NotEqualCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public bool Comparison( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) + public bool Comparison([PipedArgument] T x, T y) where T : IEquatable { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return false; - return !x.Equals(yVal); + return !x.Equals(y); } } diff --git a/Robust.Shared/Toolshed/Commands/Math/FloatCoreCommands.cs b/Robust.Shared/Toolshed/Commands/Math/FloatCoreCommands.cs index 335d984ac..aa9349eca 100644 --- a/Robust.Shared/Toolshed/Commands/Math/FloatCoreCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/FloatCoreCommands.cs @@ -160,14 +160,14 @@ public sealed class TruncCommand : ToolshedCommand public sealed class Round2FracCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation([PipedArgument] T x, [CommandArgument] int frac) + public T Operation([PipedArgument] T x, int frac) where T : IFloatingPoint { return T.Round(x, frac); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation([PipedArgument] IEnumerable x, [CommandArgument] int frac) + public IEnumerable Operation([PipedArgument] IEnumerable x, int frac) where T : IFloatingPoint => x.Select(v => Operation(v, frac)); } @@ -226,15 +226,13 @@ public sealed class SignificandBitCountCommand : ToolshedCommand public sealed class ExponentShortestBitCountCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public int Operation([PipedArgument] T x) - where T : IFloatingPoint + public int Operation([PipedArgument] T x) where T : IFloatingPoint { return x.GetExponentShortestBitLength(); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation([PipedArgument] IEnumerable x) - where T : IFloatingPoint + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : IFloatingPoint => x.Select(Operation); } @@ -242,44 +240,24 @@ public sealed class ExponentShortestBitCountCommand : ToolshedCommand public sealed class StepNextCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x - ) - where T : IFloatingPointIeee754 - { - return T.BitIncrement(x); - } + public T Operation([PipedArgument] T x) where T : IFloatingPointIeee754 + => T.BitIncrement(x); [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x - ) - where T : IFloatingPointIeee754 - => x.Select(v => Operation(ctx, v)); + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : IFloatingPointIeee754 + => x.Select(Operation); } [ToolshedCommand] public sealed class StepPrevCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x - ) - where T : IFloatingPointIeee754 - { - return T.BitDecrement(x); - } + public T Operation([PipedArgument] T x) where T : IFloatingPointIeee754 + => T.BitDecrement(x); [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x - ) - where T : IFloatingPointIeee754 - => x.Select(v => Operation(ctx, v)); + public IEnumerable Operation([PipedArgument] IEnumerable x) where T : IFloatingPointIeee754 + => x.Select(Operation); } #endregion diff --git a/Robust.Shared/Toolshed/Commands/Math/ListCommands.cs b/Robust.Shared/Toolshed/Commands/Math/ListCommands.cs index 368fa1957..fd46b9f44 100644 --- a/Robust.Shared/Toolshed/Commands/Math/ListCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/ListCommands.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Linq; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Math; @@ -9,30 +8,20 @@ public sealed class JoinCommand : ToolshedCommand { [CommandImplementation] public string Join( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] string x, - [CommandArgument] ValueRef y + string y ) { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - - return x + yVal; + return x + y; } [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable Join( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y + IEnumerable y ) { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - - return x.Concat(yVal); + return x.Concat(y); } } @@ -41,15 +30,10 @@ public sealed class AppendCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable Append( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef y + T y ) { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - - return x.Append(yVal); + return x.Append(y); } } diff --git a/Robust.Shared/Toolshed/Commands/Math/MiscOperatorCommands.cs b/Robust.Shared/Toolshed/Commands/Math/MiscOperatorCommands.cs index 076295a39..7b1973703 100644 --- a/Robust.Shared/Toolshed/Commands/Math/MiscOperatorCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/MiscOperatorCommands.cs @@ -2,17 +2,23 @@ using System.Collections.Generic; using System.Linq; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Math; -[ToolshedCommand(Name = "?"), MapLikeCommand] +[ToolshedCommand(Name = "?")] public sealed class DefaultIfNullCommand : ToolshedCommand { + // TODO TOOLSHED fix PipedBlockType + // This command will currently fail if the Tin == typeof(IEnumerable<>) + private static Type[] _parsers = [typeof(MapBlockOutputParser)]; + public override Type[] TypeParameterParsers => _parsers; + [CommandImplementation, TakesPipedTypeAsGeneric] public TOut? DefaultIfNull( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] TIn? value, - [CommandArgument] Block follower + Block follower ) where TIn : unmanaged { @@ -30,9 +36,9 @@ public sealed class OrValueCommand : ToolshedCommand [CommandImplementation, TakesPipedTypeAsGeneric] public T OrValue( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] T? value, - [CommandArgument] ValueRef alternate + ValueRef alternate ) where T : class { @@ -41,9 +47,9 @@ public sealed class OrValueCommand : ToolshedCommand [CommandImplementation, TakesPipedTypeAsGeneric] public T OrValue( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] T? value, - [CommandArgument] ValueRef alternate + ValueRef alternate ) where T : unmanaged { @@ -58,7 +64,7 @@ public sealed class DebugPrintCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public T DebugPrint( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] T value ) { @@ -68,7 +74,7 @@ public sealed class DebugPrintCommand : ToolshedCommand [CommandImplementation, TakesPipedTypeAsGeneric] public IEnumerable DebugPrint( - [CommandInvocationContext] IInvocationContext ctx, + IInvocationContext ctx, [PipedArgument] IEnumerable value ) { diff --git a/Robust.Shared/Toolshed/Commands/Math/NumAsCommand.cs b/Robust.Shared/Toolshed/Commands/Math/NumAsCommand.cs index ab4a48a85..9b876dd47 100644 --- a/Robust.Shared/Toolshed/Commands/Math/NumAsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Math/NumAsCommand.cs @@ -2,13 +2,15 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Math; [ToolshedCommand] public sealed class CheckedToCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(TypeTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public TOut Operation([PipedArgument] T x) @@ -28,7 +30,8 @@ public sealed class CheckedToCommand : ToolshedCommand [ToolshedCommand] public sealed class SaturateToCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(TypeTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public TOut Operation([PipedArgument] T x) @@ -48,7 +51,8 @@ public sealed class SaturateToCommand : ToolshedCommand [ToolshedCommand] public sealed class TruncToCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(TypeTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation, TakesPipedTypeAsGeneric] public TOut Operation([PipedArgument] T x) diff --git a/Robust.Shared/Toolshed/Commands/Math/NumberQuestionCommands.cs b/Robust.Shared/Toolshed/Commands/Math/NumberQuestionCommands.cs index d6f08c5e2..15df6d6a0 100644 --- a/Robust.Shared/Toolshed/Commands/Math/NumberQuestionCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/NumberQuestionCommands.cs @@ -102,7 +102,7 @@ public sealed class IsImaginaryCommand : ToolshedCommand // everyone on the internet is imaginary except you. [CommandImplementation] - public bool Operation([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] ICommonSession x) + public bool Operation(IInvocationContext ctx, [PipedArgument] ICommonSession x) { return ctx.Session != x; } @@ -205,7 +205,7 @@ public sealed class IsRealCommand : ToolshedCommand // nobody on the internet is real except you. [CommandImplementation] - public bool Operation([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] ICommonSession x) + public bool Operation(IInvocationContext ctx, [PipedArgument] ICommonSession x) { return ctx.Session == x; } diff --git a/Robust.Shared/Toolshed/Commands/Math/PowCommand.cs b/Robust.Shared/Toolshed/Commands/Math/PowCommand.cs index cc182345b..5aebd47d2 100644 --- a/Robust.Shared/Toolshed/Commands/Math/PowCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Math/PowCommand.cs @@ -9,29 +9,16 @@ namespace Robust.Shared.Toolshed.Commands.Math; public sealed class PowCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IPowerFunctions + public T Operation([PipedArgument] T x,T y) where T : IPowerFunctions { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return x; - return T.Pow(x, yVal); + return T.Pow(x, y); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : IPowerFunctions - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IPowerFunctions + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } diff --git a/Robust.Shared/Toolshed/Commands/Math/RngCommand.cs b/Robust.Shared/Toolshed/Commands/Math/RngCommand.cs index bdc95fd36..515b9edc7 100644 --- a/Robust.Shared/Toolshed/Commands/Math/RngCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Math/RngCommand.cs @@ -1,6 +1,5 @@ using Robust.Shared.IoC; using Robust.Shared.Random; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Math; @@ -11,48 +10,36 @@ public sealed class RngCommand : ToolshedCommand [CommandImplementation("to")] public int To( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] int from, - [CommandArgument] ValueRef to + int to ) - => _random.Next(from, to.Evaluate(ctx)); + => _random.Next(from, to); [CommandImplementation("from")] public int From( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] int to, - [CommandArgument] ValueRef from + int from ) - => _random.Next(from.Evaluate(ctx), to); + => _random.Next(from, to); [CommandImplementation("to")] public float To( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] float from, - [CommandArgument] ValueRef to + float to ) - => _random.NextFloat(from, to.Evaluate(ctx)); + => _random.NextFloat(from, to); [CommandImplementation("from")] public float From( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] float to, - [CommandArgument] ValueRef from + float from ) - => _random.NextFloat(from.Evaluate(ctx), to); + => _random.NextFloat(from, to); [CommandImplementation("prob")] public bool Prob( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] float prob ) => _random.Prob(prob); - - [CommandImplementation("prob")] - public bool Prob( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] ValueRef prob - ) - => _random.Prob(prob.Evaluate(ctx)); } diff --git a/Robust.Shared/Toolshed/Commands/Math/RootCommands.cs b/Robust.Shared/Toolshed/Commands/Math/RootCommands.cs index 385ad3bf4..ea6f9ed62 100644 --- a/Robust.Shared/Toolshed/Commands/Math/RootCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Math/RootCommands.cs @@ -42,27 +42,20 @@ public sealed class RootCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] public T Operation( - [CommandInvocationContext] IInvocationContext ctx, [PipedArgument] T x, - [CommandArgument] ValueRef y + int y ) where T : IRootFunctions { - var yVal = y.Evaluate(ctx); - return T.RootN(x, yVal); + return T.RootN(x, y); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : IRootFunctions - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IRootFunctions + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } @@ -70,29 +63,16 @@ public sealed class RootCommand : ToolshedCommand public sealed class HypotCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public T Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] T x, - [CommandArgument] ValueRef y - ) - where T : IRootFunctions + public T Operation([PipedArgument] T x, T y) where T : IRootFunctions { - var yVal = y.Evaluate(ctx); - if (yVal is null) - return default!; - return T.Hypot(x, yVal); + return T.Hypot(x, y); } [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Operation( - [CommandInvocationContext] IInvocationContext ctx, - [PipedArgument] IEnumerable x, - [CommandArgument] ValueRef> y - ) - where T : IRootFunctions - => x.Zip(y.Evaluate(ctx)!).Select(inp => + public IEnumerable Operation([PipedArgument] IEnumerable x, IEnumerable y) where T : IRootFunctions + => x.Zip(y).Select(inp => { var (left, right) = inp; - return Operation(ctx, left, new ValueRef(right)); + return Operation(left, right); }); } diff --git a/Robust.Shared/Toolshed/Commands/Misc/BuildInfoCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/BuildInfoCommand.cs index b19197100..d20ce6948 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/BuildInfoCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/BuildInfoCommand.cs @@ -13,7 +13,7 @@ internal sealed class BuildInfoCommand : ToolshedCommand private static readonly string Gold = Color.Gold.ToHex(); [CommandImplementation] - public void BuildInfo([CommandInvocationContext] IInvocationContext ctx) + public void BuildInfo(IInvocationContext ctx) { var game = _cfg.GetCVar(CVars.BuildForkId); var buildCommit = _cfg.GetCVar(CVars.BuildHash); diff --git a/Robust.Shared/Toolshed/Commands/Misc/CmdCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/CmdCommand.cs index 9c32f165e..096aeef03 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/CmdCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/CmdCommand.cs @@ -9,7 +9,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc; public sealed class CmdCommand : ToolshedCommand { [CommandImplementation("list")] - public IEnumerable List([CommandInvocationContext] IInvocationContext ctx) + public IEnumerable List(IInvocationContext ctx) => ctx.Environment.AllCommands(); [CommandImplementation("moo")] @@ -20,15 +20,15 @@ public sealed class CmdCommand : ToolshedCommand public string GetLocStr([PipedArgument] CommandSpec cmd) => cmd.DescLocStr(); [CommandImplementation("info")] - public CommandSpec Info([CommandArgument] CommandSpec cmd) => cmd; + public CommandSpec Info(CommandSpec cmd) => cmd; #if CLIENT_SCRIPTING [CommandImplementation("getshim")] - public MethodInfo GetShim([CommandArgument] Block block) + public MethodInfo GetShim(Block block) { // this is gross sue me - var invocable = block.CommandRun.Commands.Last().Item1.Invocable; + var invocable = block.Run.Commands.Last().Item1.Invocable; return invocable.GetMethodInfo(); } #endif diff --git a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs index 2a35fb01c..741f96d8e 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/ExplainCommand.cs @@ -1,4 +1,6 @@ -using Robust.Shared.Toolshed.Syntax; +using System.Text; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.Commands.Misc; @@ -7,15 +9,39 @@ public sealed class ExplainCommand : ToolshedCommand { [CommandImplementation] public void Explain( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] CommandRun expr + IInvocationContext ctx, + CommandRun expr ) { + var builder = new StringBuilder(); 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(""); + var name = cmd.Implementor.FullName; + builder.AppendLine($"{name} - {cmd.Implementor.Description()}"); + + if (cmd.PipedType != null) + { + var pipeArg = cmd.Method.Base.PipeArg; + DebugTools.AssertNotNull(pipeArg); + builder.Append($"<{pipeArg?.Name} ({ToolshedCommandImplementor.GetFriendlyName(cmd.PipedType)})> -> "); + } + + if (cmd.Bundle.Inverted) + builder.Append("not "); + + builder.Append($"{name}"); + foreach (var (argName, argType, _) in cmd.Method.Args) + { + builder.Append($" <{argName} ({ToolshedCommandImplementor.GetFriendlyName(argType)})>"); + } + + builder.AppendLine(); + var piped = cmd.PipedType?.PrettyName() ?? "[none]"; + var returned = cmd.ReturnType?.PrettyName() ?? "[none]"; + builder.AppendLine($"{piped} -> {returned}"); + builder.AppendLine(); } + + ctx.WriteLine(builder.ToString()); } } diff --git a/Robust.Shared/Toolshed/Commands/Misc/MoreCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/MoreCommand.cs index 91cb66e11..bf7365407 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/MoreCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/MoreCommand.cs @@ -4,7 +4,7 @@ public sealed class MoreCommand : ToolshedCommand { [CommandImplementation] - public object? More([CommandInvocationContext] IInvocationContext ctx) + public object? More(IInvocationContext ctx) { return ctx.ReadVar("more"); } diff --git a/Robust.Shared/Toolshed/Commands/Misc/SearchCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/SearchCommand.cs index 04ebbb3ae..29404e5b3 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/SearchCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/SearchCommand.cs @@ -11,7 +11,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc; public sealed class SearchCommand : ToolshedCommand { [CommandImplementation, TakesPipedTypeAsGeneric] - public IEnumerable Search([PipedArgument] IEnumerable input, [CommandArgument] string term) + public IEnumerable Search([PipedArgument] IEnumerable input, string term) { var list = input.Select(x => Toolshed.PrettyPrintType(x, out _)).ToList(); return list.Where(x => x.Contains(term, StringComparison.InvariantCultureIgnoreCase)).Select(x => diff --git a/Robust.Shared/Toolshed/Commands/Misc/StopwatchCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/StopwatchCommand.cs index cef6e212b..6c5e2042d 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/StopwatchCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/StopwatchCommand.cs @@ -8,7 +8,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc; public sealed class StopwatchCommand : ToolshedCommand { [CommandImplementation] - public object? Stopwatch([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] CommandRun expr) + public object? Stopwatch(IInvocationContext ctx, CommandRun expr) { var watch = new Stopwatch(); watch.Start(); diff --git a/Robust.Shared/Toolshed/Commands/Misc/TypesCommand.cs b/Robust.Shared/Toolshed/Commands/Misc/TypesCommand.cs index ef526ee2f..068d06619 100644 --- a/Robust.Shared/Toolshed/Commands/Misc/TypesCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Misc/TypesCommand.cs @@ -7,7 +7,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc; internal sealed class TypesCommand : ToolshedCommand { [CommandImplementation("consumers")] - public void Consumers([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] object? input) + public void Consumers(IInvocationContext ctx, [PipedArgument] object? input) { var t = input is Type ? (Type)input : input!.GetType(); @@ -23,7 +23,7 @@ internal sealed class TypesCommand : ToolshedCommand } [CommandImplementation("tree")] - public IEnumerable Tree([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] object? input) + public IEnumerable Tree(IInvocationContext ctx, [PipedArgument] object? input) { var t = input is Type ? (Type)input : input!.GetType(); return Toolshed.AllSteppedTypes(t); diff --git a/Robust.Shared/Toolshed/Commands/Players/SelfCommand.cs b/Robust.Shared/Toolshed/Commands/Players/SelfCommand.cs index ba6823bf0..570f927b8 100644 --- a/Robust.Shared/Toolshed/Commands/Players/SelfCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Players/SelfCommand.cs @@ -7,7 +7,7 @@ namespace Robust.Shared.Toolshed.Commands.Players; internal sealed class SelfCommand : ToolshedCommand { [CommandImplementation] - public EntityUid Self([CommandInvocationContext] IInvocationContext ctx) + public EntityUid Self(IInvocationContext ctx) { if (ctx.Session is null) { diff --git a/Robust.Shared/Toolshed/Commands/Types/MethodsCommand.cs b/Robust.Shared/Toolshed/Commands/Types/MethodsCommand.cs index d8286d0c5..844e9a216 100644 --- a/Robust.Shared/Toolshed/Commands/Types/MethodsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Types/MethodsCommand.cs @@ -34,7 +34,7 @@ internal sealed class MethodsCommand : ToolshedCommand } [CommandImplementation("overridesfrom")] - public IEnumerable OverridesFrom([PipedArgument] IEnumerable types, [CommandArgument] Type t) + public IEnumerable OverridesFrom([PipedArgument] IEnumerable types, Type t) { foreach (var ty in types) { diff --git a/Robust.Shared/Toolshed/Commands/Values/ConstantCommands.cs b/Robust.Shared/Toolshed/Commands/Values/ConstantCommands.cs index e5d4725ec..11e69ead2 100644 --- a/Robust.Shared/Toolshed/Commands/Values/ConstantCommands.cs +++ b/Robust.Shared/Toolshed/Commands/Values/ConstantCommands.cs @@ -4,26 +4,26 @@ public sealed class IntCommand : ToolshedCommand { [CommandImplementation] - public int Impl([CommandArgument] int value) => value; + public int Impl(int value) => value; } [ToolshedCommand(Name = "f")] public sealed class FloatCommand : ToolshedCommand { [CommandImplementation] - public float Impl([CommandArgument] float value) => value; + public float Impl(float value) => value; } [ToolshedCommand(Name = "s")] public sealed class StringCommand : ToolshedCommand { [CommandImplementation] - public string Impl([CommandArgument] string value) => value; + public string Impl(string value) => value; } [ToolshedCommand(Name = "b")] public sealed class BoolCommand : ToolshedCommand { [CommandImplementation] - public bool Impl([CommandArgument] bool value) => value; + public bool Impl(bool value) => value; } diff --git a/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs b/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs index 7e96f2e27..a46d86b18 100644 --- a/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Values/EntCommand.cs @@ -1,5 +1,4 @@ using Robust.Shared.GameObjects; -using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.Commands.Values; @@ -7,6 +6,5 @@ namespace Robust.Shared.Toolshed.Commands.Values; internal sealed class EntCommand : ToolshedCommand { [CommandImplementation] - public EntityUid Ent([CommandArgument] ValueRef ent, [CommandInvocationContext] IInvocationContext ctx) => ent.Evaluate(ctx); + public EntityUid Ent(EntityUid uid) => uid; } - diff --git a/Robust.Shared/Toolshed/Commands/Values/ValCommand.cs b/Robust.Shared/Toolshed/Commands/Values/ValCommand.cs index 2c9ae72ee..59414aa2c 100644 --- a/Robust.Shared/Toolshed/Commands/Values/ValCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Values/ValCommand.cs @@ -1,16 +1,15 @@ using System; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; namespace Robust.Shared.Toolshed.Commands.Values; [ToolshedCommand] public sealed class ValCommand : ToolshedCommand { - public override Type[] TypeParameterParsers => new[] {typeof(Type)}; + private static Type[] _parsers = [typeof(TypeTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; [CommandImplementation] - public T Val( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] ValueRef value - ) => value.Evaluate(ctx)!; + public T Val(T value) => value; } diff --git a/Robust.Shared/Toolshed/Commands/Values/VarCommand.cs b/Robust.Shared/Toolshed/Commands/Values/VarCommand.cs new file mode 100644 index 000000000..da182bc8c --- /dev/null +++ b/Robust.Shared/Toolshed/Commands/Values/VarCommand.cs @@ -0,0 +1,20 @@ +using System; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Robust.Shared.Toolshed.Commands.Values; + +/// +/// Variant of the that only works for variable references, and automatically infers the type +/// from the variable's value. +/// +[ToolshedCommand] +public sealed class VarCommand : ToolshedCommand +{ + private static Type[] _parsers = [typeof(VarTypeParser)]; + public override Type[] TypeParameterParsers => _parsers; + + [CommandImplementation] + public T Var(IInvocationContext ctx, VarRef var) + => var.Evaluate(ctx)!; +} diff --git a/Robust.Shared/Toolshed/Commands/Vfs/CdCommand.cs b/Robust.Shared/Toolshed/Commands/Vfs/CdCommand.cs index 2081ab908..0250c2f5d 100644 --- a/Robust.Shared/Toolshed/Commands/Vfs/CdCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Vfs/CdCommand.cs @@ -6,10 +6,7 @@ namespace Robust.Shared.Toolshed.Commands.Vfs; internal sealed class CdCommand : VfsCommand { [CommandImplementation] - public void Cd( - [CommandInvocationContext] IInvocationContext ctx, - [CommandArgument] ResPath path - ) + public void Cd(IInvocationContext ctx,ResPath path) { var curPath = CurrentPath(ctx); diff --git a/Robust.Shared/Toolshed/Commands/Vfs/LsCommand.cs b/Robust.Shared/Toolshed/Commands/Vfs/LsCommand.cs index 2e28b678e..7bca5dd9f 100644 --- a/Robust.Shared/Toolshed/Commands/Vfs/LsCommand.cs +++ b/Robust.Shared/Toolshed/Commands/Vfs/LsCommand.cs @@ -8,14 +8,14 @@ namespace Robust.Shared.Toolshed.Commands.Vfs; internal sealed class LsCommand : VfsCommand { [CommandImplementation("here")] - public IEnumerable LsHere([CommandInvocationContext] IInvocationContext ctx) + public IEnumerable LsHere(IInvocationContext ctx) { var curPath = CurrentPath(ctx); return Resources.ContentGetDirectoryEntries(curPath).Select(x => curPath/x); } [CommandImplementation("in")] - public IEnumerable LsIn([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] ResPath @in) + public IEnumerable LsIn(IInvocationContext ctx, ResPath @in) { var curPath = CurrentPath(ctx); if (@in.IsRooted) diff --git a/Robust.Shared/Toolshed/Errors/IConError.cs b/Robust.Shared/Toolshed/Errors/IConError.cs index ba1eef31b..39a8bb5ce 100644 --- a/Robust.Shared/Toolshed/Errors/IConError.cs +++ b/Robust.Shared/Toolshed/Errors/IConError.cs @@ -5,6 +5,13 @@ using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.Errors; +// TODO TOOLSHED Localize Errors +// Requires reworking the IConError interface to take in an ILocalizationManager + +// TODO TOOLSHED Rework IConError +// A bunch of the errors are structs, but they get boxed anyways. So might as well make them all inherit from a base +// class, so that we don't need to constantly re-define the properties. + /// /// A Toolshed-oriented representation of an error. /// Contains metadata about where in an executed command it occurred, and supports formatting. @@ -100,6 +107,13 @@ public static class ConHelpers { var msg = FormattedMessage.FromUnformatted(input[..span.X]); msg.PushColor(color); + if (span.Y >= input.Length) + { + msg.AddText(input[span.X..]); + msg.Pop(); + return msg; + } + msg.AddText(input[span.X..span.Y]); msg.Pop(); msg.AddText(input[span.Y..]); @@ -119,3 +133,12 @@ public static class ConHelpers return FormattedMessage.FromUnformatted(builder.ToString()); } } + +public abstract class ConError : IConError +{ + public abstract FormattedMessage DescribeInner(); + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} diff --git a/Robust.Shared/Toolshed/IInvocationContext.cs b/Robust.Shared/Toolshed/IInvocationContext.cs index 0fbfa00a6..68bed9623 100644 --- a/Robust.Shared/Toolshed/IInvocationContext.cs +++ b/Robust.Shared/Toolshed/IInvocationContext.cs @@ -24,11 +24,7 @@ public interface IInvocationContext /// public bool CheckInvokable(CommandSpec command, out IConError? error) { - if (Toolshed.ActivePermissionController is { } controller) - return controller.CheckInvokable(command, Session, out error); - - error = null; - return true; + return Toolshed.CheckInvokable(command, Session, out error); } ToolshedEnvironment Environment { get; } @@ -115,6 +111,8 @@ public interface IInvocationContext /// public IEnumerable GetErrors(); + public bool HasErrors { get; } + /// /// Clears the list of unobserved errors. /// @@ -123,14 +121,6 @@ public interface IInvocationContext /// public void ClearErrors(); - /// - /// The backing variable storage. - /// - /// - /// You don't have to use this at all. - /// - protected Dictionary Variables { get; } - /// /// Reads the given variable from the context. /// @@ -139,11 +129,7 @@ public interface IInvocationContext /// /// This may behave arbitrarily, but it's advised it behave somewhat sanely. /// - public virtual object? ReadVar(string name) - { - Variables.TryGetValue(name, out var res); - return res; - } + object? ReadVar(string name); /// /// Writes the given variable to the context. @@ -153,17 +139,24 @@ public interface IInvocationContext /// /// Writes may be ignored or manipulated. /// - public virtual void WriteVar(string name, object? value) - { - Variables[name] = value; - } + void WriteVar(string name, object? value); + + /// + /// Whether or not a variable is read-only. Used for variable name auto-completion. + /// + bool IsReadonlyVar(string name) => false; /// /// Provides a list of all variables that have been written to at some point. /// /// List of all variables. - public virtual IEnumerable GetVars() + public IEnumerable GetVars(); +} + +public sealed class ReadonlyVariableError(string name) : ConError +{ + public override FormattedMessage DescribeInner() { - return Variables.Keys; + return FormattedMessage.FromUnformatted($"${name} is a read-only variable."); } } diff --git a/Robust.Shared/Toolshed/IPermissionController.cs b/Robust.Shared/Toolshed/IPermissionController.cs index 98eacb1a1..b4ca4391e 100644 --- a/Robust.Shared/Toolshed/IPermissionController.cs +++ b/Robust.Shared/Toolshed/IPermissionController.cs @@ -1,5 +1,4 @@ -using JetBrains.Annotations; -using Robust.Shared.Player; +using Robust.Shared.Player; using Robust.Shared.Toolshed.Errors; namespace Robust.Shared.Toolshed; diff --git a/Robust.Shared/Toolshed/Invocation/OldShellInvocationContext.cs b/Robust.Shared/Toolshed/Invocation/OldShellInvocationContext.cs index d6c0f997e..d2a6fedd8 100644 --- a/Robust.Shared/Toolshed/Invocation/OldShellInvocationContext.cs +++ b/Robust.Shared/Toolshed/Invocation/OldShellInvocationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Robust.Shared.Console; using Robust.Shared.IoC; using Robust.Shared.Network; @@ -23,6 +24,12 @@ internal sealed class OldShellInvocationContext : IInvocationContext /// public IConsoleShell? Shell; + /// + public NetUserId? User { get; } + + /// + public ICommonSession? Session => Shell?.Player; + public OldShellInvocationContext(IConsoleShell shell) { IoCManager.InjectDependencies(this); @@ -30,12 +37,6 @@ internal sealed class OldShellInvocationContext : IInvocationContext User = Session?.UserId; } - /// - public NetUserId? User { get; } - - /// - public ICommonSession? Session => Shell?.Player; - /// public void WriteLine(string line) { @@ -60,6 +61,8 @@ internal sealed class OldShellInvocationContext : IInvocationContext return _errors; } + public bool HasErrors => _errors.Count > 0; + /// public void ClearErrors() { @@ -67,6 +70,33 @@ internal sealed class OldShellInvocationContext : IInvocationContext } /// + public object? ReadVar(string name) + { + if (name == "self" && Session?.AttachedEntity is { } ent) + return ent; + return Variables.GetValueOrDefault(name); + } + + /// + public void WriteVar(string name, object? value) + { + if (name == "self") + ReportError(new ReadonlyVariableError("self")); + else + Variables[name] = value; + } + + /// + public bool IsReadonlyVar(string name) => name == "self"; + + /// + public IEnumerable GetVars() + { + return Session?.AttachedEntity != null + ? Variables.Keys.Append("self") + : Variables.Keys; + } + public Dictionary Variables { get; } = new(); } diff --git a/Robust.Shared/Toolshed/LocalVarInvocationContext.cs b/Robust.Shared/Toolshed/LocalVarInvocationContext.cs new file mode 100644 index 000000000..64d6149c4 --- /dev/null +++ b/Robust.Shared/Toolshed/LocalVarInvocationContext.cs @@ -0,0 +1,98 @@ +using System.Collections.Generic; +using Robust.Shared.Network; +using Robust.Shared.Player; +using Robust.Shared.Toolshed.Errors; + +namespace Robust.Shared.Toolshed; + +/// +/// that wraps some other context and provides some local variables. +/// +public sealed class LocalVarInvocationContext(IInvocationContext inner) : IInvocationContext +{ + public bool CheckInvokable(CommandSpec command, out IConError? error) + { + return inner.CheckInvokable(command, out error); + } + + public ICommonSession? Session => inner.Session; + public ToolshedManager Toolshed => inner.Toolshed; + public NetUserId? User => inner.User; + public ToolshedEnvironment Environment => inner.Environment; + + public void WriteLine(string line) => inner.WriteLine(line); + public void ReportError(IConError err) => inner.ReportError(err); + public IEnumerable GetErrors() => inner.GetErrors(); + public bool HasErrors => inner.HasErrors; + public void ClearErrors() => inner.ClearErrors(); + + public Dictionary LocalVars = new(); + public HashSet? ReadonlyVars; + + public void SetLocal(string name, object? value) + { + LocalVars[name] = value; + } + + public void SetLocal(string name, object? value, bool @readonly) + { + LocalVars[name] = value; + SetReadonly(name, @readonly); + } + + public void SetReadonly(string name, bool @readonly) + { + if (@readonly) + { + ReadonlyVars ??= new(); + ReadonlyVars.Add(name); + } + else + { + ReadonlyVars?.Remove(name); + } + } + + public void ClearLocal(string name) + { + LocalVars.Remove(name); + ReadonlyVars?.Remove(name); + } + + public object? ReadVar(string name) + { + return LocalVars.TryGetValue(name, out var obj) + ? obj + : inner.ReadVar(name); + } + + public void WriteVar(string name, object? value) + { + if (ReadonlyVars != null && ReadonlyVars.Contains(name)) + { + ReportError(new ReadonlyVariableError(name)); + return; + } + + if (LocalVars.ContainsKey(name)) + LocalVars[name] = value; + else + inner.WriteVar(name, value); + } + + public bool IsReadonlyVar(string name) => ReadonlyVars != null && ReadonlyVars.Contains(name); + + public IEnumerable GetVars() + { + foreach (var key in LocalVars.Keys) + { + yield return key; + } + + foreach (var key in inner.GetVars()) + { + if (!LocalVars.ContainsKey(key)) + yield return key; + } + } +} diff --git a/Robust.Shared/Toolshed/ReflectionExtensions.cs b/Robust.Shared/Toolshed/ReflectionExtensions.cs index 042a648de..bdf87e84b 100644 --- a/Robust.Shared/Toolshed/ReflectionExtensions.cs +++ b/Robust.Shared/Toolshed/ReflectionExtensions.cs @@ -5,6 +5,9 @@ using System.Linq.Expressions; using System.Reflection; using Robust.Shared.Exceptions; using Robust.Shared.Log; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; +using Robust.Shared.Utility; namespace Robust.Shared.Toolshed; @@ -64,24 +67,6 @@ internal static class ReflectionExtensions } } - public static IEnumerable<(string, List)> BySubCommand(this IEnumerable methods) - { - var output = new Dictionary>(); - - foreach (var method in methods) - { - var subCommand = method.GetCustomAttribute()!.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) @@ -101,6 +86,53 @@ internal static class ReflectionExtensions return t.GetGenericTypeDefinition().MakeGenericType(newArgs); } + public static bool HasGenericParent(this Type type, Type parent) + { + DebugTools.Assert(parent.IsGenericType); + var t = type; + while (t != null) + { + if (t.IsGenericType(parent)) + return true; + + t = t.BaseType; + } + + return false; + } + + public static bool IsValueRef(this Type type) + { + return type.HasGenericParent(typeof(ValueRef<>)); + } + + public static bool IsCustomParser(this Type type) + { + return type.HasGenericParent(typeof(CustomTypeParser<>)); + } + + public static bool IsParser(this Type type) + { + return type.HasGenericParent(typeof(TypeParser<>)); + } + + public static bool IsCommandArgument(this ParameterInfo param) + { + if (param.HasCustomAttribute()) + return true; + + if (param.HasCustomAttribute()) + return false; + + if (param.HasCustomAttribute()) + return false; + + if (param.HasCustomAttribute()) + return false; + + return param.ParameterType != typeof(IInvocationContext); + } + public static string PrettyName(this Type type) { var name = type.Name; @@ -128,13 +160,12 @@ internal static class ReflectionExtensions public static ParameterInfo? ConsoleGetPipedArgument(this MethodInfo method) { - var p = method.GetParameters().Where(x => x.GetCustomAttribute() is not null).ToList(); - return p.FirstOrDefault(); + return method.GetParameters().SingleOrDefault(x => x.HasCustomAttribute()); } - public static IEnumerable ConsoleGetArguments(this MethodInfo method) + public static bool ConsoleHasInvertedArgument(this MethodInfo method) { - return method.GetParameters().Where(x => x.GetCustomAttribute() is not null); + return method.GetParameters().Any(x => x.HasCustomAttribute()); } public static Expression CreateEmptyExpr(this Type t) @@ -159,6 +190,28 @@ internal static class ReflectionExtensions throw new NotImplementedException(); } + // IEnumerable ^ IEnumerable -> EntityUid + public static Type Intersect(this Type left, Type right) + { + if (!left.IsGenericType) + return left; + + if (!right.IsGenericType) + return left; + + var leftGen = left.GetGenericTypeDefinition(); + var rightGen = right.GetGenericTypeDefinition(); + var leftArgs = left.GetGenericArguments(); + + // TODO TOOLSHED implement this properly. + // Currently this only recurses through the first generic argument. + + if (leftGen == rightGen) + return Intersect(leftArgs.First(), right.GenericTypeArguments.First()); + + return Intersect(leftArgs.First(), right); + } + public static void DumpGenericInfo(this Type t) { Logger.Debug($"Info for {t.PrettyName()}"); diff --git a/Robust.Shared/Toolshed/Syntax/Block.cs b/Robust.Shared/Toolshed/Syntax/Block.cs index b6d3264a0..cc26ca4e1 100644 --- a/Robust.Shared/Toolshed/Syntax/Block.cs +++ b/Robust.Shared/Toolshed/Syntax/Block.cs @@ -1,143 +1,129 @@ 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 +/// +/// A simple block of commands. +/// +[Virtual] +public class Block(CommandRun expr) { - internal CommandRun CommandRun { get; set; } + public readonly CommandRun Run = expr; - public static bool TryParse( - bool doAutoComplete, - ParserContext parserContext, - Type? pipedType, - [NotNullWhen(true)] out Block? block, - out ValueTask<(CompletionResult?, IConError?)>? autoComplete, - out IConError? error - ) + public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? block) { - parserContext.ConsumeWhitespace(); - - var enclosed = parserContext.EatMatch('{'); - - if (enclosed) - parserContext.PushTerminator("}"); - - CommandRun.TryParse(doAutoComplete, parserContext, pipedType, null, !enclosed, out var expr, out autoComplete, out error); - - if (expr is null) - { - block = null; + block = null; + if (!TryParseBlock(ctx, null, null, out var run)) return false; - } - block = new Block(expr); + block = new Block(run); return true; } - public Block(CommandRun expr) - { - CommandRun = expr; - } - public object? Invoke(object? input, IInvocationContext ctx) { - return CommandRun.Invoke(input, ctx); + return Run.Invoke(input, ctx); + } + + public static bool TryParseBlock( + ParserContext ctx, + Type? pipedType, + Type? targetOutput, + [NotNullWhen(true)] out CommandRun? run) + { + run = null; + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + + ctx.ConsumeWhitespace(); + if (!ctx.EatMatch('{')) + { + if (ctx.GenerateCompletions) + ctx.Completions = CompletionResult.FromOptions([new CompletionOption("{")]); + else + ctx.Error = new MissingOpeningBrace(); + + return false; + } + + ctx.PushBlockTerminator('}'); + if (!CommandRun.TryParse(ctx, pipedType, targetOutput, out run)) + { + return false; + } + + if (ctx.EatBlockTerminator()) + return true; + + ctx.ConsumeWhitespace(); + if (!ctx.GenerateCompletions) + { + ctx.Error = new MissingClosingBrace(); + return false; + } + + if (ctx.OutOfInput) + ctx.Completions = CompletionResult.FromOptions([new CompletionOption("}")]); + return false; + } + + public override string ToString() + { + return $"{{ {Run} }}"; } } /// -/// Something more akin to actual expressions. +/// A block of commands that take in no input, and return . /// -public sealed class Block +[Virtual] +public class Block(CommandRun expr) : Block(expr) { - internal CommandRun CommandRun { get; set; } - - public static bool TryParse(bool doAutoComplete, ParserContext parserContext, Type? pipedType, - [NotNullWhen(true)] out Block? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error) + public static bool TryParse(ParserContext ctx, + [NotNullWhen(true)] out Block? block + ) { - parserContext.ConsumeWhitespace(); - - var enclosed = parserContext.EatMatch('{'); - - if (enclosed) - parserContext.PushTerminator("}"); - - CommandRun.TryParse(enclosed, doAutoComplete, parserContext, pipedType, !enclosed, out var expr, out autoComplete, out error); - - if (expr is null) - { - block = null; + block = null; + if (!TryParseBlock(ctx, null, typeof(T), out var run)) return false; - } - block = new Block(expr); + block = new Block(run); return true; } - public Block(CommandRun expr) + public T? Invoke(IInvocationContext ctx) { - CommandRun = expr; - } - - public T? Invoke(object? input, IInvocationContext ctx) - { - return CommandRun.Invoke(input, ctx); + var res = Run.Invoke(null, ctx); + if (res is null) + return default; + return (T?) res; } } -public sealed class Block +/// +/// A block of commands that take in , and return . +/// +[Virtual] +public class Block(CommandRun expr) : Block(expr) { - internal CommandRun CommandRun { get; set; } - - public static bool TryParse(bool doAutoComplete, ParserContext parserContext, Type? pipedType, - [NotNullWhen(true)] out Block? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error) + public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? block) { - parserContext.ConsumeWhitespace(); - - var enclosed = parserContext.EatMatch('{'); - - if (enclosed) - parserContext.PushTerminator("}"); - - CommandRun.TryParse(enclosed, doAutoComplete, parserContext, !enclosed, out var expr, out autoComplete, out error); - - if (expr is null) - { - block = null; + block = null; + if (!TryParseBlock(ctx, typeof(TIn), typeof(TOut), out var run)) return false; - } - block = new Block(expr); + block = new Block(run); return true; } - public Block(CommandRun expr) - { - CommandRun = expr; - } - public TOut? Invoke(TIn? input, IInvocationContext ctx) { - return CommandRun.Invoke(input, ctx); + var res = Run.Invoke(input, ctx); + if (res is null) + return default; + return (TOut?) res; } } - - -public record struct MissingClosingBrace() : IConError -{ - public FormattedMessage DescribeInner() - { - return FormattedMessage.FromUnformatted("Expected a closing brace."); - } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } -} diff --git a/Robust.Shared/Toolshed/Syntax/Expression.cs b/Robust.Shared/Toolshed/Syntax/Expression.cs index 5e603a8c0..b43a23bab 100644 --- a/Robust.Shared/Toolshed/Syntax/Expression.cs +++ b/Robust.Shared/Toolshed/Syntax/Expression.cs @@ -2,84 +2,172 @@ 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; -/// -/// A "run" of commands. Not a true expression. -/// public sealed class CommandRun { - public readonly List<(ParsedCommand, Vector2i)> Commands; - private readonly string _originalExpr; + /// + /// The original string that contains the substring from which this command run was parsed. + /// + public readonly string OriginalExpr; - public static bool TryParse(bool doAutocomplete, - ParserContext parserContext, + /// + /// The list of parsed commands, along with the start and end indices in + /// + public readonly List<(ParsedCommand, Vector2i)> Commands; + + #region Misc Debug Properties + + /// + /// The type returned by the last command in + /// + public readonly Type? ReturnType; + + /// + /// The type that should get piped into the first command in + /// + public readonly Type? PipedType; + + /// + /// The starting index of the first command in + /// + public readonly int StartIndex; + + /// + /// The ending index of the last command in + /// + public readonly int EndIndex; + + /// + /// The substring of from which all of the commands in the run were parsed. + /// + public string SubExpr => OriginalExpr[StartIndex..EndIndex]; + + #endregion + + public CommandRun(List<(ParsedCommand, Vector2i)> commands, string originalExpr, Type? returnType, Type? pipedType) + { + DebugTools.Assert(commands.Count > 0); + OriginalExpr = originalExpr; + Commands = commands; + ReturnType = returnType; + PipedType = pipedType; + StartIndex = commands[0].Item2.X; + EndIndex = commands[^1].Item2.Y; + DebugTools.Assert(StartIndex >= 0); + DebugTools.Assert(EndIndex <= OriginalExpr.Length); + DebugTools.Assert(EndIndex > StartIndex); + } + + /// + /// Attempt to parse a sequence of commands that initially take in the given piped type. + /// + /// The parser context + /// The type of object being piped into the command that we want to parse, This determines which commands are valid. Null means that the first command takes no piped input + /// The desired output type of the final command in the sequence. Null implies no constraint. The type implies that the final command should not return a value + /// The expression that was generated + /// + public static bool TryParse( + ParserContext ctx, Type? pipedType, Type? targetOutput, - bool once, - [NotNullWhen(true)] out CommandRun? expr, - out ValueTask<(CompletionResult?, IConError?)>? autocomplete, - out IConError? error) + [NotNullWhen(true)] out CommandRun? expr) { - autocomplete = null; - error = null; + expr = null; var cmds = new List<(ParsedCommand, Vector2i)>(); - var start = parserContext.Index; - var noCommand = false; - parserContext.ConsumeWhitespace(); + var start = ctx.Index; + ctx.ConsumeWhitespace(); + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + if (pipedType == typeof(void)) + throw new ArgumentException($"Piped type cannot be void"); - while ((!once || cmds.Count < 1) && ParsedCommand.TryParse(doAutocomplete, parserContext, pipedType, out var cmd, out error, out noCommand, out autocomplete, targetOutput)) + if (ctx.PeekBlockTerminator()) { - var end = parserContext.Index; + // Trying to parse an empty block as a command run? I.e. " { } " + ctx.Error = new EmptyCommandRun(); + ctx.Error.Contextualize(ctx.Input, new(start, ctx.Index + 1)); + return false; + } + + while (true) + { + if (!ParsedCommand.TryParse(ctx, pipedType, out var cmd)) + { + if (ctx.Error is NotValidCommandError err) + err.TargetType = targetOutput; + return false; + } + pipedType = cmd.ReturnType; - cmds.Add((cmd, (start, end))); - parserContext.ConsumeWhitespace(); - start = parserContext.Index; + cmds.Add((cmd, (start, ctx.Index))); + ctx.ConsumeWhitespace(); - if (parserContext.EatTerminator()) + if (ctx.EatCommandTerminators()) + { + ctx.ConsumeWhitespace(); + pipedType = null; + } + + // If the command run encounters a block terminator we exit out. + // The parser that pushed the block terminator is what should actually eat & pop it, so that it can + // return appropriate errors if the block was not terminated. + if (ctx.PeekBlockTerminator()) break; - // Prevent auto completions from dumping a list of all commands at the end of any complete command. - if (parserContext.Index > parserContext.MaxIndex) + if (ctx.OutOfInput) break; + + start = ctx.Index; + + if (pipedType != typeof(void)) + continue; + + // The previously parsed command does not generate any output that can be piped/chained into another + // command. This can happen if someone tries to provide more arguments than a command accepts. + // e.g., " i 5 5". In this case, the parsing fails and should make it clear that no more input was expected. + // Multiple unrelated commands on a single line are still supported via the ';' terminator. + // I.e., "i 5 i 5" is invalid, but "i 5; i 5" is valid. + // IMO the latter is also easier to read. + if (ctx.GenerateCompletions) + return false; + + ctx.Error = new EndOfCommandError(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index+1)); + return false; } - if (error is OutOfInputError && noCommand) - error = null; - - if (error is not null and not OutOfInputError || error is OutOfInputError && !noCommand || cmds.Count == 0) + if (ctx.Error != null || cmds.Count == 0) { expr = null; return false; } - if (!(cmds.Last().Item1.ReturnType?.IsAssignableTo(targetOutput) ?? false) && targetOutput is not null) + // Return the last type, even if the command ended with a ';' + var returnType = cmds[^1].Item1.ReturnType; + if (targetOutput != null && !returnType.IsAssignableTo(targetOutput)) { - error = new ExpressionOfWrongType(targetOutput, cmds.Last().Item1.ReturnType!, once); + ctx.Error = new WrongCommandReturn(targetOutput, returnType); expr = null; return false; } - expr = new CommandRun(cmds, parserContext.Input); + expr = new CommandRun(cmds, ctx.Input, returnType, pipedType); return true; } public object? Invoke(object? input, IInvocationContext ctx, bool reportErrors = true) { - // TODO TOOLSHED - // improve error handling. Most expression invokers don't bother to check for errors. + // TODO TOOLSHED Improve error handling + // Most expression invokers don't bother to check for errors. // This especially applies to all map / emplace / sort commands. // A simple error while enumerating entities could lock up the server. - if (ctx.GetErrors().Any()) + if (ctx.HasErrors) { // Attempt to prevent O(n^2) growth in errors due to people repeatedly evaluating expressions without // checking for errors. @@ -90,27 +178,27 @@ public sealed class CommandRun 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()); - } + if (!ctx.HasErrors) + continue; + if (!reportErrors) return null; + + 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) + public override string ToString() { - Commands = commands; - _originalExpr = originalExpr; + return SubExpr; } } @@ -118,11 +206,9 @@ public sealed class CommandRun { internal readonly CommandRun InnerCommandRun; - public static bool TryParse(bool blockMode, bool doAutoComplete, ParserContext parserContext, bool once, - [NotNullWhen(true)] out CommandRun? expr, - out ValueTask<(CompletionResult?, IConError?)>? autocomplete, out IConError? error) + public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? expr) { - if (!CommandRun.TryParse(doAutoComplete, parserContext, typeof(TIn), typeof(TOut), once, out var innerExpr, out autocomplete, out error)) + if (!CommandRun.TryParse(ctx, typeof(TIn), typeof(TOut), out var innerExpr)) { expr = null; return false; @@ -140,21 +226,27 @@ public sealed class CommandRun return (TOut?) res; } - private CommandRun(CommandRun commandRun) + internal CommandRun(CommandRun commandRun) { InnerCommandRun = commandRun; } + + public override string ToString() + { + return InnerCommandRun.ToString(); + } } public sealed class CommandRun { - internal readonly CommandRun _innerCommandRun; + internal readonly CommandRun InnerCommandRun; - public static bool TryParse(bool blockMode, bool doAutoComplete, ParserContext parserContext, Type? pipedType, bool once, - [NotNullWhen(true)] out CommandRun? expr, out ValueTask<(CompletionResult?, IConError?)>? completion, - out IConError? error) + public static bool TryParse( + ParserContext ctx, + Type? pipedType, + [NotNullWhen(true)] out CommandRun? expr) { - if (!CommandRun.TryParse(doAutoComplete, parserContext, pipedType, typeof(TRes), once, out var innerExpr, out completion, out error)) + if (!CommandRun.TryParse(ctx, pipedType, typeof(TRes), out var innerExpr)) { expr = null; return false; @@ -166,30 +258,29 @@ public sealed class CommandRun public TRes? Invoke(object? input, IInvocationContext ctx) { - var res = _innerCommandRun.Invoke(input, ctx); + var res = InnerCommandRun.Invoke(input, ctx); if (res is null) return default; return (TRes?) res; } - private CommandRun(CommandRun commandRun) + internal CommandRun(CommandRun commandRun) { - _innerCommandRun = commandRun; + InnerCommandRun = commandRun; + } + + public override string ToString() + { + return InnerCommandRun.ToString(); } } -public record struct ExpressionOfWrongType(Type Expected, Type Got, bool Once) : IConError +public record struct WrongCommandReturn(Type Expected, Type Got) : IConError { public FormattedMessage DescribeInner() { var msg = FormattedMessage.FromUnformatted( - $"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."); - } + $"Expected an command run that returns type {Expected.PrettyName()}, but got {Got.PrettyName()}"); return msg; } @@ -198,3 +289,50 @@ public record struct ExpressionOfWrongType(Type Expected, Type Got, bool Once) : public Vector2i? IssueSpan { get; set; } public StackTrace? Trace { get; set; } } + +public sealed class EmptyCommandRun : IConError +{ + public FormattedMessage DescribeInner() + { + var msg = FormattedMessage.FromUnformatted($"Empty command block"); + + return msg; + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} + + +public record struct MissingClosingBrace : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted("Expected a closing brace, }."); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} + +public record struct MissingOpeningBrace : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted("Expected an opening brace, {."); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} + +public sealed class EndOfCommandError : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted("Expected an end of command (;)"); + } +} diff --git a/Robust.Shared/Toolshed/Syntax/IVariableParser.cs b/Robust.Shared/Toolshed/Syntax/IVariableParser.cs new file mode 100644 index 000000000..2b47007dd --- /dev/null +++ b/Robust.Shared/Toolshed/Syntax/IVariableParser.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Runtime.CompilerServices; +using Robust.Shared.Console; + +namespace Robust.Shared.Toolshed.Syntax; + +/// +/// Interface for attempting to infer the type of a variable while parsing toolshed commands. +/// +/// +/// The variable parser being used by the may change depending on which command/block is +/// currently being parsed. E.g., if a command has a variable confined to a command block, it might use a +/// +public interface IVariableParser +{ + public static readonly IVariableParser Empty = new EmptyVarParser(); + + /// + /// Attempt to get the type of the variable with the given name. + /// + bool TryParseVar(string name, [NotNullWhen(true)] out Type? type); + + /// + /// Generate completion options containing valid variable names along with their types. + /// + CompletionResult GenerateCompletions(bool includeReadonly = true) + { + var vars = GetVars() + .Where(x => includeReadonly || !IsReadonlyVar(x.Item1)) + .Select(x => new CompletionOption($"${x.Item1}", $"{x.Item2.PrettyName()}")); + return CompletionResult.FromHintOptions(vars, ""); + } + + /// + /// Generate completion options containing valid variable names along with their types. + /// + CompletionResult GenerateCompletions(bool includeReadonly = true) + { + var vars = GetVars() + .Where(x => x.Item2 == typeof(T)) + .Where(x => includeReadonly || !IsReadonlyVar(x.Item1)) + .Select(x => new CompletionOption($"${x.Item1}")); + + return CompletionResult.FromHintOptions(vars,$""); + } + + /// + /// Whether or not a variable is read-only. Used for variable name auto-completion. + /// + bool IsReadonlyVar(string name) => false; + + public IEnumerable<(string, Type)> GetVars(); + + private sealed class EmptyVarParser : IVariableParser + { + public bool TryParseVar(string name, [NotNullWhen(true)] out Type? type) + { + type = null; + return false; + } + + public IEnumerable<(string, Type)> GetVars() + { + yield break; + } + } +} + +/// +/// Infer the variable type from the value currently saved to an invocation context. +/// This is only valid if no other command that has been parsed so far could modify the stored value once invoked. +/// If a command can modify the variable's type, it should instead use a . +/// +public sealed class InvocationCtxVarParser(IInvocationContext ctx) : IVariableParser +{ + private readonly IInvocationContext _ctx = ctx; + + public bool TryParseVar(string name, [NotNullWhen(true)] out Type? type) + { + type = _ctx.ReadVar(name)?.GetType(); + return type != null; + } + + public IEnumerable<(string, Type)> GetVars() + { + foreach (var name in _ctx.GetVars()) + { + if (TryParseVar(name, out var type)) + yield return (name, type); + } + } + + public bool IsReadonlyVar(string name) => _ctx.IsReadonlyVar(name); +} + +/// +/// Simple wrapper around a variable type parser that modifies / overrides the types returned by some other parser. +/// +public sealed class LocalVarParser(IVariableParser inner) : IVariableParser +{ + public readonly IVariableParser Inner = inner; + + public Dictionary? Variables; + public HashSet? ReadonlyVariables; + + public void SetLocalType(string name, Type? type, bool @readonly) + { + if (type == null) + { + Variables?.Remove(name); + return; + } + + Variables ??= new(); + Variables[name] = type; + + if (@readonly) + { + ReadonlyVariables ??= new(); + ReadonlyVariables.Add(name); + } + } + + public bool TryParseVar(string name, [NotNullWhen(true)] out Type? type) + { + if (Variables != null && Variables.TryGetValue(name, out type)) + return true; + + return Inner.TryParseVar(name, out type); + } + + public IEnumerable<(string, Type)> GetVars() + { + foreach (var (name, type) in Variables!) + { + yield return (name, type); + } + + foreach (var (name, type) in Inner.GetVars()) + { + if (!Variables.ContainsKey(name)) + yield return (name, type); + } + } + + public bool IsReadonlyVar(string name) + { + return ReadonlyVariables != null && ReadonlyVariables.Contains(name); + } +} diff --git a/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs b/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs index c061fe62d..5e7765222 100644 --- a/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs +++ b/Robust.Shared/Toolshed/Syntax/ParsedCommand.cs @@ -2,9 +2,7 @@ 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; @@ -15,199 +13,230 @@ using Invocable = Func; public sealed class ParsedCommand { - public ToolshedCommand Command { get; } - public Type? ReturnType { get; } + public ToolshedCommand Command => Implementor.Owner; + public Type ReturnType => Method.Info.ReturnType; - public Type? PipedType => Bundle.PipedArgumentType; + public Type? PipedType => Bundle.PipedType; + public string? SubCommand => Bundle.SubCommand; + + internal readonly ToolshedCommandImplementor Implementor; internal Invocable Invocable { get; } internal CommandArgumentBundle Bundle { get; } - public string? SubCommand { get; } - public static bool TryParse( - bool doAutoComplete, - ParserContext parserContext, - Type? pipedArgumentType, - [NotNullWhen(true)] out ParsedCommand? result, - out IConError? error, - out bool noCommand, - out ValueTask<(CompletionResult?, IConError?)>? autocomplete, - Type? targetType = null - ) + internal readonly ConcreteCommandMethod Method; + + public static bool TryParse(ParserContext ctx, Type? piped, [NotNullWhen(true)] out ParsedCommand? result) { - noCommand = false; - var checkpoint = parserContext.Save(); - var bundle = new CommandArgumentBundle() - {Arguments = new(), Inverted = false, PipedArgumentType = pipedArgumentType, TypeArguments = Array.Empty()}; + var checkpoint = ctx.Save(); + var oldBundle = ctx.Bundle; + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + ctx.Bundle = new CommandArgumentBundle + { + Inverted = false, + PipedType = piped + }; - autocomplete = null; - if (!TryDigestModifiers(parserContext, bundle, out error) - || !TryParseCommand(doAutoComplete, parserContext, 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) - ) + ctx.ConsumeWhitespace(); + + if (!TryDigestModifiers(ctx)) { result = null; - parserContext.Restore(checkpoint); + ctx.Restore(checkpoint); return false; } + // TODO TOOLSHED + // completion suggestions for modifiers? + // I.e., if parsing a command name fails, we should take into account that they might be trying to type out + // "not" or some other command modifier? - result = new(bundle, invocable, command, retType, subCommand); + if (!TryParseCommand(ctx, out var invocable, out var method, out var implementor)) + { + result = null; + ctx.Restore(checkpoint); + return false; + } + + // No errors or completions should have been generated if the parse was successful. + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + result = new(ctx.Bundle, invocable, method.Value, implementor); + ctx.Bundle = oldBundle; return true; } - private ParsedCommand(CommandArgumentBundle bundle, Invocable invocable, ToolshedCommand command, Type? returnType, string? subCommand) + private ParsedCommand(CommandArgumentBundle bundle, Invocable invocable, ConcreteCommandMethod method, ToolshedCommandImplementor implementor) { Invocable = invocable; Bundle = bundle; - Command = command; - ReturnType = returnType; - SubCommand = subCommand; + Implementor = implementor; + Method = method; } - private static bool TryDigestModifiers(ParserContext parserContext, CommandArgumentBundle bundle, out IConError? error) + /// + /// Attempt to process any modifer tokens that modify how a command behaves or how it's arguments are parsed and + /// store the results in the . + /// + private static bool TryDigestModifiers(ParserContext ctx) { - error = null; - if (parserContext.PeekWord() == "not") + if (ctx.EatMatch("not")) { - parserContext.GetWord(); //yum - bundle.Inverted = true; + ctx.ConsumeWhitespace(); + ctx.Bundle.Inverted = true; } return true; } private static bool TryParseCommand( - bool makeCompletions, - ParserContext parserContext, - 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 - ) + ParserContext ctx, + [NotNullWhen(true)] out Invocable? invocable, + [NotNullWhen(true)] out ConcreteCommandMethod? method, + [NotNullWhen(true)] out ToolshedCommandImplementor? implementor) { - noCommand = false; - var start = parserContext.Index; - var cmd = parserContext.GetWord(ParserContext.IsCommandToken); - subCommand = null; invocable = null; - command = null; - if (cmd is null) - { - if (parserContext.PeekRune() is null) - { - noCommand = true; - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - autocomplete = null; - if (makeCompletions) - { - var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void)); - autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""), error)); - } + implementor = null; + method = null; + var cmdNameStart = ctx.Index; + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + // Try to parse the command name + if (!TryParseCommandName(ctx, out var cmdName)) + return false; + + // Attempt to find the command with the given name + if (!ctx.Environment.TryGetCommand(cmdName, out var command)) + { + if (ctx.GenerateCompletions) + { + if (ctx.OutOfInput) + ctx.Completions = ctx.Environment.CommandCompletionsForType(ctx.Bundle.PipedType); return false; } + + ctx.Error ??= new UnknownCommandError(cmdName); + ctx.Error.Contextualize(ctx.Input, (cmdNameStart, ctx.Index)); + return false; + } + + // Attempt to parse the subcommand, if applicable. + if (!TryParseImplementor(ctx, command, out implementor)) + return false; + + // This is a safeguard to try help prevent information from being accidentally leaked by poorly validated + // auto completion for commands. I.e., if there is a command that operates on all minds/players, we don't want + // to send the client a list of all players. + if (!ctx.CheckInvokable(implementor.Spec)) + { + if (ctx.GenerateCompletions) + ctx.Completions = CompletionResult.FromHint($"Insufficient permissions for command: {implementor.FullName}"); + return false; + } + + // If the name command is currently still being typed, we continue to give command name completions, not + // argument completions. + if (ctx.GenerateCompletions && ctx.OutOfInput) + { + ctx.Completions = ctx.Bundle.SubCommand == null + ? ctx.Environment.CommandCompletionsForType(ctx.Bundle.PipedType) + : ctx.Environment.SubCommandCompletionsForType(ctx.Bundle.PipedType, command); + + // TODO TOOLSHED invalid-fail + // This technically "fails" to parse what might otherwise be a valid command that takes no argument. + // However this only happens when generating completions, not when actually executing the command + // Still, this is pretty janky and I don't know of a good fix. + return false; + } + + return implementor.TryParse(ctx, out invocable, out method); + } + + private static bool TryParseCommandName(ParserContext ctx, [NotNullWhen(true)] out string? name) + { + var cmdNameStart = ctx.Index; + name = ctx.GetWord(ParserContext.IsCommandToken); + if (name != null) + { + ctx.Bundle.Command = name; + return true; + } + + if (ctx.OutOfInput) + { + if (ctx.GenerateCompletions) + { + ctx.Completions = ctx.Environment.CommandCompletionsForType(ctx.Bundle.PipedType); + } else { - - noCommand = true; - error = new NotValidCommandError(targetType); - error.Contextualize(parserContext.Input, (start, parserContext.Index+1)); - autocomplete = null; - return false; - } - } - - if (!parserContext.Environment.TryGetCommand(cmd, out var cmdImpl)) - { - error = new UnknownCommandError(cmd); - error.Contextualize(parserContext.Input, (start, parserContext.Index)); - autocomplete = null; - if (makeCompletions) - { - var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void)); - autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""), error)); + ctx.Error = new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index)); } return false; } - if (cmdImpl.HasSubCommands) + if (ctx.GenerateCompletions) + return false; + + ctx.Error = new NotValidCommandError(); + ctx.Error.Contextualize(ctx.Input, (cmdNameStart, ctx.Index+1)); + return false; + } + + private static bool TryParseImplementor(ParserContext ctx, ToolshedCommand cmd, [NotNullWhen(true)] out ToolshedCommandImplementor? impl) + { + if (!cmd.HasSubCommands) { - error = null; - autocomplete = null; - if (makeCompletions) - { - var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void)).Where(x => x.Cmd.Name == cmd); - autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>(( - CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""), error)); - } - - if (parserContext.GetChar() is not ':') - { - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - return false; - } - - var subCmdStart = parserContext.Index; - - if (parserContext.GetWord(ParserContext.IsToken) is not { } subcmd) - { - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - return false; - } - - if (!cmdImpl.Subcommands.Contains(subcmd)) - { - error = new UnknownSubcommandError(cmd, subcmd, cmdImpl); - error.Contextualize(parserContext.Input, (subCmdStart, parserContext.Index)); - return false; - } - - subCommand = subcmd; + impl = cmd.CommandImplementors[string.Empty]; + return true; } - if (parserContext.ConsumeWhitespace() == 0 && makeCompletions) + impl = null; + if (!ctx.EatMatch(':')) { - error = null; - var cmds = parserContext.Environment.CommandsTakingType(pipedType ?? typeof(void)); - autocomplete = ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""), null)); + if (ctx.GenerateCompletions) + { + ctx.Completions = ctx.Environment.SubCommandCompletionsForType(ctx.Bundle.PipedType, cmd); + return false; + } + ctx.Error = new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index)); return false; } - var argsStart = parserContext.Index; - - if (!cmdImpl.TryParseArguments(makeCompletions, parserContext, pipedType, subCommand, out var args, out var types, out error, out autocomplete)) + var subCmdStart = ctx.Index; + if (ctx.GetWord(ParserContext.IsToken) is not { } subcmd) { - error?.Contextualize(parserContext.Input, (argsStart, parserContext.Index)); + if (ctx.GenerateCompletions) + { + ctx.Completions = ctx.Environment.SubCommandCompletionsForType(ctx.Bundle.PipedType, cmd); + return false; + } + ctx.Error = new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index)); return false; } - bundle.TypeArguments = types; - - if (!cmdImpl.TryGetImplementation(bundle.PipedArgumentType, subCommand, types, out var impl)) + if (!cmd.CommandImplementors.TryGetValue(subcmd, out impl!)) { - error = new NoImplementationError(cmd, types, subCommand, bundle.PipedArgumentType, parserContext.Environment); - error.Contextualize(parserContext.Input, (start, parserContext.Index)); - autocomplete = null; + if (ctx.GenerateCompletions) + { + ctx.Completions = ctx.Environment.SubCommandCompletionsForType(ctx.Bundle.PipedType, cmd); + return false; + } + ctx.Error = new UnknownSubcommandError(subcmd, cmd); + ctx.Error.Contextualize(ctx.Input, (subCmdStart, ctx.Index)); return false; } - bundle.Arguments = args; - invocable = impl; - command = cmdImpl; - autocomplete = null; + ctx.Bundle.SubCommand = subcmd; return true; } - private bool _passedInvokeTest = false; + private bool _passedInvokeTest; public object? Invoke(object? pipedIn, IInvocationContext ctx) { @@ -248,16 +277,22 @@ public record struct UnknownCommandError(string Cmd) : IConError public StackTrace? Trace { get; set; } } -public record NoImplementationError(string Cmd, Type[] Types, string? SubCommand, Type? PipedType, ToolshedEnvironment ctx) : IConError +public sealed class NoImplementationError(ParserContext ctx) : ConError { - public FormattedMessage DescribeInner() + public readonly ToolshedEnvironment Env = ctx.Environment; + public readonly string Cmd = ctx.Bundle.Command!; + public readonly string? SubCommand = ctx.Bundle.SubCommand; + public readonly Type[]? Types = ctx.Bundle.TypeArguments; + public readonly Type? PipedType = ctx.Bundle.PipedType; + + public override FormattedMessage DescribeInner() { var msg = FormattedMessage.FromUnformatted($"Could not find an implementation for {Cmd} given the input type {PipedType?.PrettyName() ?? "void"}."); msg.PushNewline(); var typeArgs = ""; - if (Types.Length != 0) + if (Types != null && Types.Length != 0) { typeArgs = "<" + string.Join(",", Types.Select(ReflectionExtensions.PrettyName)) + ">"; } @@ -265,12 +300,12 @@ public record NoImplementationError(string Cmd, Type[] Types, string? SubCommand msg.AddText($"Signature: {Cmd}{(SubCommand is not null ? $":{SubCommand}" : "")}{typeArgs} {PipedType?.PrettyName() ?? "void"} -> ???"); var piped = PipedType ?? typeof(void); - var cmdImpl = ctx.GetCommand(Cmd); - var accepted = cmdImpl.AcceptedTypes(SubCommand).ToHashSet(); + var cmdImpl = Env.GetCommand(Cmd); + var accepted = cmdImpl.AcceptedTypes(SubCommand); - foreach (var (command, subCommand) in ctx.CommandsTakingType(piped)) + foreach (var (command, subCommand) in Env.CommandsTakingType(piped)) { - if (!command.TryGetReturnType(subCommand, piped, Array.Empty(), out var retType) || !accepted.Any(x => retType.IsAssignableTo(x))) + if (!command.TryGetReturnType(subCommand, piped, null, out var retType) || !accepted.Any(x => retType.IsAssignableTo(x))) continue; if (!cmdImpl.TryGetReturnType(SubCommand, retType, Types, out var myRetType)) @@ -284,18 +319,14 @@ public record NoImplementationError(string Cmd, Type[] Types, string? SubCommand return msg; } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } } -public record UnknownSubcommandError(string Cmd, string SubCmd, ToolshedCommand Command) : IConError +public record UnknownSubcommandError(string SubCmd, ToolshedCommand Command) : IConError { public FormattedMessage DescribeInner() { var msg = new FormattedMessage(); - msg.AddText($"The command group {Cmd} doesn't have command {SubCmd}."); + msg.AddText($"The command group {Command.Name} doesn't have command {SubCmd}."); msg.PushNewline(); msg.AddText($"The valid commands are: {string.Join(", ", Command.Subcommands)}."); return msg; @@ -306,9 +337,11 @@ public record UnknownSubcommandError(string Cmd, string SubCmd, ToolshedCommand public StackTrace? Trace { get; set; } } -public record NotValidCommandError(Type? TargetType) : IConError +public sealed class NotValidCommandError : ConError { - public FormattedMessage DescribeInner() + public Type? TargetType; + + public override FormattedMessage DescribeInner() { var msg = new FormattedMessage(); msg.AddText("Ran into an invalid command, could not parse."); @@ -320,8 +353,4 @@ public record NotValidCommandError(Type? TargetType) : IConError return msg; } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } } diff --git a/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs b/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs index e4d866fe8..2d0836810 100644 --- a/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs +++ b/Robust.Shared/Toolshed/Syntax/ParserContext.Config.cs @@ -6,22 +6,29 @@ public sealed partial class ParserContext { public bool NoMultilineExprs = false; - public static bool IsToken(Rune c) - => (Rune.IsLetter(c) || Rune.IsDigit(c) || c == new Rune('_')) && !Rune.IsWhiteSpace(c); + public static bool IsToken(Rune c) => Rune.IsLetterOrDigit(c) || c == new Rune('_'); public static bool IsCommandToken(Rune c) - => - c != new Rune('{') - && c != new Rune('}') - && c != new Rune('[') - && c != new Rune(']') - && c != new Rune('(') - && c != new Rune(')') - && c != new Rune('"') - && c != new Rune('\'') - && c != new Rune(':') - && !Rune.IsWhiteSpace(c) - && !Rune.IsControl(c); + { + if (Rune.IsLetterOrDigit(c)) + return true; + + if (Rune.IsWhiteSpace(c)) + return false; + + return c != new Rune('{') + && c != new Rune('}') + && c != new Rune('[') + && c != new Rune(']') + && c != new Rune('(') + && c != new Rune(')') + && c != new Rune('"') + && c != new Rune('\'') + && c != new Rune(':') + && c != new Rune(';') + && c != new Rune('$') + && !Rune.IsControl(c); + } public static bool IsNumeric(Rune c) => @@ -30,7 +37,4 @@ public sealed partial class ParserContext || c == new Rune('-') || c == new Rune('.') || c == new Rune('%'); - - public static bool IsTerminator(Rune c) - => (Rune.IsSymbol(c) || Rune.IsPunctuation(c) || Rune.IsSeparator(c) || c == new Rune('}')) && !Rune.IsWhiteSpace(c); } diff --git a/Robust.Shared/Toolshed/Syntax/ParserContext.cs b/Robust.Shared/Toolshed/Syntax/ParserContext.cs index bf2a4308e..9f20c56e3 100644 --- a/Robust.Shared/Toolshed/Syntax/ParserContext.cs +++ b/Robust.Shared/Toolshed/Syntax/ParserContext.cs @@ -1,13 +1,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Numerics; using System.Text; using System.Text.RegularExpressions; using JetBrains.Annotations; using Robust.Shared.Collections; -using Robust.Shared.IoC; +using Robust.Shared.Console; using Robust.Shared.Log; using Robust.Shared.Maths; +using Robust.Shared.Player; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Utility; @@ -18,17 +20,66 @@ public sealed partial class ParserContext public readonly ToolshedManager Toolshed; public readonly ToolshedEnvironment Environment; + /// + /// The parser to use when trying autocomplete variable names or to infer the type of a variable. + /// + /// + /// Unless a command uses custom parsing code, the parser context will be unaware if a command modifies a variable's + /// type during invocation. As a result, autocompletion may be inaccurate, and invocation may cause a + /// if the command that was parsed relied on knowing a variable's type. + /// + public IVariableParser VariableParser; + + /// + /// Arguments for the command that is currently being parsed. Useful for parsing context dependent types. E.g., + /// command type arguments that depend on the piped type. + /// + public CommandArgumentBundle Bundle; + + /// + /// Whether or not to generate auto-completion options. + /// + public bool GenerateCompletions; + + /// + /// Any auto-completion suggestions that have been generated while parsing. + /// + public CompletionResult? Completions; + + /// + /// Any errors that have come up while parsing. This is generally null while is true, + /// under the assumption that the command is purely being parsed to gather completion suggestions, not to try evaluate it. + /// + public IConError? Error; + public readonly string Input; - public int MaxIndex { get; private set; } + public int MaxIndex { get; } - public int Index { get; private set; } = 0; + public int Index { get; private set; } - public ParserContext(string input, ToolshedManager toolshed, ToolshedEnvironment? environment = null) + public readonly ICommonSession? Session; + + /// + /// Whether the parser has reached the end of the input. + /// + public bool OutOfInput => Index > MaxIndex; + + public ParserContext(string input, ToolshedManager toolshed, ToolshedEnvironment environment, IVariableParser parser, ICommonSession? session) { Toolshed = toolshed; - Environment = environment ?? toolshed.DefaultEnvironment; + Environment = environment; Input = input; MaxIndex = input.Length - 1; + VariableParser = parser; + Session = session; + } + + public ParserContext(string input, ToolshedManager toolshed) : this(input, toolshed, toolshed.DefaultEnvironment, IVariableParser.Empty, null) + { + } + + public ParserContext(string input, ToolshedManager toolshed, IInvocationContext ctx) : this(input, toolshed, ctx.Environment, new InvocationCtxVarParser(ctx), ctx.Session) + { } private ParserContext(ParserContext parserContext, int sliceSize, int? index) @@ -39,35 +90,31 @@ public sealed partial class ParserContext Input = parserContext.Input; Index = index ?? parserContext.Index; MaxIndex = Math.Min(parserContext.MaxIndex, Index + sliceSize - 1); - } - - public bool SpanInRange(int length) - { - return MaxIndex >= (Index + length - 1); + VariableParser = parserContext.VariableParser; + Session = parserContext.Session; } public bool EatMatch(char c) => EatMatch(new Rune(c)); public bool EatMatch(Rune c) { - if (PeekRune() == c) - { - GetRune(); - return true; - } + if (PeekRune() is not { } next || next != c) + return false; - return false; + Index += c.Utf16SequenceLength; + return true; } public bool EatMatch(string c) { - if (PeekWord() == c) - { - GetWord(); - return true; - } + // TODO TOOLSHED Optimize + // Combine into one method, remove allocations. + // I.e., this unnecessarily creates two strings. + if (PeekWord() != c) + return false; - return false; + GetWord(); + return true; } /// @@ -88,7 +135,7 @@ public sealed partial class ParserContext public Rune? PeekRune() { - if (!SpanInRange(1)) + if (MaxIndex < Index) return null; return Rune.GetRuneAt(Input, Index); @@ -96,13 +143,11 @@ public sealed partial class ParserContext public Rune? GetRune() { - if (PeekRune() is { } c) - { - Index += c.Utf16SequenceLength; - return c; - } + if (PeekRune() is not { } c) + return null; - return null; + Index += c.Utf16SequenceLength; + return c; } /// @@ -110,26 +155,24 @@ public sealed partial class ParserContext /// public char? GetChar() { - if (PeekRune() is { } c) - { - Index += c.Utf16SequenceLength; + if (PeekRune() is not { } c) + return null; - if (c.Utf16SequenceLength > 1) - return '\x01'; + Index += c.Utf16SequenceLength; - Span buffer = stackalloc char[2]; - c.EncodeToUtf16(buffer); + if (c.Utf16SequenceLength > 1) + return '\x01'; - return buffer[0]; - } + Span buffer = stackalloc char[2]; + c.EncodeToUtf16(buffer); - return null; + return buffer[0]; } [PublicAPI] public void DebugPrint() { - Logger.DebugS("parser", string.Join(", ", _terminatorStack)); + Logger.DebugS("parser", string.Join(", ", _blockStack)); Logger.DebugS("parser", Input); MakeDebugPointer(Index); MakeDebugPointer(MaxIndex, '|'); @@ -228,13 +271,15 @@ public sealed partial class ParserContext public ParserRestorePoint Save() { - return new ParserRestorePoint(Index, new(_terminatorStack)); + return new ParserRestorePoint(Index, new(_blockStack), Bundle, VariableParser); } public void Restore(ParserRestorePoint point) { Index = point.Index; - _terminatorStack = point.TerminatorStack; + _blockStack = point.TerminatorStack; + Bundle = point.Bundle; + VariableParser =point.VariableParser; } public int ConsumeWhitespace() @@ -244,37 +289,34 @@ public sealed partial class ParserContext return Consume(Rune.IsWhiteSpace); } - private Stack _terminatorStack = new(); + private Stack _blockStack = new(); - public void PushTerminator(string term) + public void PushBlockTerminator(Rune term) { - _terminatorStack.Push(term); + _blockStack.Push(term); } - public bool PeekTerminated() + public void PushBlockTerminator(char term) + => PushBlockTerminator(new Rune(term)); + + public bool PeekBlockTerminator() { - if (_terminatorStack.Count == 0) + if (_blockStack.Count == 0) return false; - ConsumeWhitespace(); - var save = Save(); - var match = TryMatch(_terminatorStack.Peek()); - Restore(save); - return match; + return PeekRune() == _blockStack.Peek(); } - public bool EatTerminator() + public bool EatBlockTerminator() { - if (_terminatorStack.Count == 0) + if (_blockStack.Count == 0) return false; - if (TryMatch(_terminatorStack.Peek())) - { - _terminatorStack.Pop(); - return true; - } + if (!EatMatch(_blockStack.Peek())) + return false; - return false; + _blockStack.Pop(); + return true; } public bool CheckEndLine() @@ -290,7 +332,7 @@ public sealed partial class ParserContext while (PeekRune() is { } c && control(c)) { - GetRune(); + Index += c.Utf16SequenceLength; amount++; } @@ -334,20 +376,91 @@ public sealed partial class ParserContext return new ParserContext(this, Index - blockStart, blockStart); } -} -public readonly struct ParserRestorePoint -{ - public readonly int Index; - internal readonly Stack TerminatorStack; - - public ParserRestorePoint(int index, Stack terminatorStack) + /// + /// Check whether a command can be invoked by the given session/user. + /// A null session implies that the command is being run by the server. + /// + public bool CheckInvokable(CommandSpec cmd) { - Index = index; - TerminatorStack = terminatorStack; + return Toolshed.CheckInvokable(cmd, Session, out Error); + } + + /// + /// Check whether all commands implemented by some type can be invoked by the given session/user. + /// A null session implies that the command is being run by the server. + /// + public bool CheckInvokable() where T : ToolshedCommand + { + if (!Environment.TryGetCommands(out var list)) + return false; + + foreach (var x in list) + { + if (!CheckInvokable(x)) + return false; + } + + return true; + } + + public bool PeekCommandOrBlockTerminated() + { + if (PeekRune() is not { } c) + return false; + + if (c == new Rune(';')) + return true; + + if (NoMultilineExprs && c == new Rune('\n')) + return true; + + if (_blockStack.Count == 0) + return false; + + return c == _blockStack.Peek(); + } + + /// + /// Attempts to consume a single command terminator, which is either a ';' or a newline (if is + /// enabled). + /// + public bool EatCommandTerminator() + { + if (EatMatch(new Rune(';'))) + return true; + + // If multi-line commands are not enabled, we treat a newline like a ';' + // I.e., it terminates the command currently being parsed in + return NoMultilineExprs && EatMatch(new Rune('\n')); + } + + /// + /// Attempts to repeatedly consume command terminators, and return true if any were consumed. + /// + public bool EatCommandTerminators() + { + if (!EatCommandTerminator()) + return false; + + // Maybe one day we want to allow ';;' to have special meaning? + // But for now, just eat em all. + ConsumeWhitespace(); + while (EatCommandTerminator()) + { + ConsumeWhitespace(); + } + + return true; } } +public readonly record struct ParserRestorePoint( + int Index, + Stack TerminatorStack, + CommandArgumentBundle Bundle, + IVariableParser VariableParser); + public record OutOfInputError : IConError { public FormattedMessage DescribeInner() diff --git a/Robust.Shared/Toolshed/Syntax/ValueRef.cs b/Robust.Shared/Toolshed/Syntax/ValueRef.cs index 2716cb60d..580aa983b 100644 --- a/Robust.Shared/Toolshed/Syntax/ValueRef.cs +++ b/Robust.Shared/Toolshed/Syntax/ValueRef.cs @@ -6,107 +6,108 @@ using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.Syntax; -public sealed class ValueRef : ValueRef +/// +/// This class is used represent toolshed command arguments that are either a reference to a Toolshed variable +/// (), a block of commands that need to be evaluated (), or simply some +/// specific value that has already been parsed/evaluated (). +/// +public abstract class ValueRef { - public ValueRef(ValueRef 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 abstract T? Evaluate(IInvocationContext ctx); - public ValueRef(Block innerBlock) : base(innerBlock) - { - } - - public ValueRef(T value) : base(value) + // Internal method used when invoking commands to evaluate & cast command parameters. + // Mainly exists for convenience, as expression trees don't support 'is' pattern matching or null propagation. + // Also makes makes for much nicer debugging of command parameter parsing + internal static T? EvaluateParameter(object? obj, IInvocationContext ctx) { + return obj switch + { + null => default, + T cast => cast, + ValueRef @ref => @ref.Evaluate(ctx), + _ => throw new Exception( + $"Failed to parse command parameter. This likely is a toolshed bug and should be reported.\n" + + $"Target type: {typeof(T).PrettyName()}.\n" + + $"Input type: {obj.GetType()}.\n" + + $"Input: {obj}") + }; } } -[Virtual] -public class ValueRef +[Obsolete("Use EntProtoId / ProtoId")] +public sealed class ValueRef(ValueRef inner) : ValueRef { - internal Block? InnerBlock; - internal string? VarName; - internal bool HasValue = false; - internal T? Value; - internal string? Expression; - internal Vector2i? RefSpan; - - protected ValueRef() + public override T? Evaluate(IInvocationContext ctx) { + return inner.Evaluate(ctx); } +} - public ValueRef(string varName) +public sealed class BlockRef(Block block) : ValueRef +{ + public override T? Evaluate(IInvocationContext ctx) => block.Invoke(ctx); +} + +/// +/// This class is used represent toolshed command arguments that references to a Toolshed variable. +/// I.e., something accessible via . +/// +public sealed class VarRef(string varName) : ValueRef +{ + /// + /// The name of the variable. + /// + public readonly string VarName = varName; + + public override T? Evaluate(IInvocationContext ctx) { - VarName = varName; - } - - public ValueRef(Block 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) - { - var value = ctx.ReadVar(VarName); - - if (value is not T v) - { - ctx.ReportError(new BadVarTypeError(value?.GetType() ?? typeof(void), typeof(T), VarName)); - return default; - } + var value = ctx.ReadVar(VarName); + if (value is T v) return v; - } - else if (InnerBlock is not null) - { - return InnerBlock.Invoke(null, ctx); - } - else - { - throw new UnreachableException(); - } + + var error = new BadVarTypeError(value?.GetType(), typeof(T), VarName); + ctx.ReportError(error); + return default; } - public void Set(IInvocationContext ctx, T? value) + public record BadVarTypeError(Type? Got, Type Expected, string VarName) : IConError { - if (VarName is null) - throw new NotImplementedException(); + public FormattedMessage DescribeInner() + { + var msg = Got == null + ? $"Variable ${VarName} is not assigned. Expected variable of type {Expected.PrettyName()}." + : $"Variable ${VarName} is not of the expected type. Expected {Expected.PrettyName()} but got {Got?.PrettyName()}."; + return FormattedMessage.FromUnformatted(msg); + } - ctx.WriteVar(VarName!, value); + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } } } -public record BadVarTypeError(Type Got, Type Expected, string VarName) : IConError +// Used to only parse writeable variable names. +// Hacky class to work around the lack of generics in attributes, preventing a custom type parser . +public sealed class WriteableVarRef(VarRef inner) : ValueRef { - public FormattedMessage DescribeInner() + public readonly VarRef Inner = inner; + public override T? Evaluate(IInvocationContext ctx) { - return FormattedMessage.FromUnformatted($"Got unexpected type {Got.PrettyName()} in {VarName}, expected {Expected.PrettyName()}"); + return Inner.Evaluate(ctx); + } +} + +/// +/// This class represents a command argument that simply corresponds to a specific value of +/// some type that has already been parsed/evaluated. +/// +internal sealed class ParsedValueRef(T? value) : ValueRef +{ + public readonly T? Value = value; + + public override T? Evaluate(IInvocationContext ctx) + { + return Value; } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.Entities.cs b/Robust.Shared/Toolshed/ToolshedCommand.Entities.cs index 7f6ed2d8d..5152201aa 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.Entities.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.Entities.cs @@ -154,6 +154,10 @@ public abstract partial class ToolshedCommand where T: EntitySystem => EntitySystemManager.GetEntitySystem(); + // GetSys is just too many letters to type + [PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)] + protected T Sys() where T: EntitySystem => EntitySystemManager.GetEntitySystem(); + /// /// A shorthand for retrieving an entity query. /// diff --git a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs index 719bdd595..f827d15a1 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.Help.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.Help.cs @@ -1,8 +1,4 @@ -using System; -using System.Linq; -using System.Text; - -namespace Robust.Shared.Toolshed; +namespace Robust.Shared.Toolshed; public abstract partial class ToolshedCommand { @@ -10,21 +6,18 @@ public abstract partial class ToolshedCommand /// Returns a command's localized description. /// public string Description(string? subCommand) - => Loc.GetString(UnlocalizedDescription(subCommand)); + { + CommandImplementors.TryGetValue(subCommand ?? string.Empty, out var impl); + return impl?.Description() ?? string.Empty; + } /// /// Returns the locale string for a command's description. /// - public string UnlocalizedDescription(string? subCommand) + public string DescriptionLocKey(string? subCommand) { - if (Name.All(char.IsAsciiLetterOrDigit)) - { - return $"command-description-{Name}" + (subCommand is not null ? $"-{subCommand}" : ""); - } - else - { - return $"command-description-{GetType().PrettyName()}" + (subCommand is not null ? $"-{subCommand}" : ""); - } + CommandImplementors.TryGetValue(subCommand ?? string.Empty, out var impl); + return impl?.DescriptionLocKey() ?? string.Empty; } /// @@ -32,65 +25,12 @@ public abstract partial class ToolshedCommand /// public string GetHelp(string? subCommand) { - // Description - var description = subCommand is null - ? $"{Name}: {Description(null)}" - : $"{Name}:{subCommand}: {Description(subCommand)}"; - - // Usage - var usage = new StringBuilder(); - usage.AppendLine(); - usage.Append(Loc.GetString("command-description-usage")); - foreach (var (pipedType, parameters) in _readonlyParameters[subCommand ?? ""]) - { - usage.Append(Environment.NewLine + " "); - - // Piped type - if (pipedType != null) - { - usage.Append(Loc.GetString("command-description-usage-pipedtype", - ("typeName", GetFriendlyName(pipedType)))); - } - - // Name - usage.Append(Name); - - // Parameters - foreach (var param in parameters) - { - usage.Append($" <{GetFriendlyName(param)}>"); - } - } - - return description + usage; + CommandImplementors.TryGetValue(subCommand ?? string.Empty, out var impl); + return impl?.GetHelp() ?? string.Empty; } - /// public override string ToString() { - return GetHelp(null); - } - - public static string GetFriendlyName(Type type) - { - string friendlyName = type.Name; - if (type.IsGenericType) - { - int iBacktick = friendlyName.IndexOf('`'); - if (iBacktick > 0) - { - friendlyName = friendlyName.Remove(iBacktick); - } - friendlyName += "<"; - Type[] typeParameters = type.GetGenericArguments(); - for (int i = 0; i < typeParameters.Length; ++i) - { - string typeParamName = GetFriendlyName(typeParameters[i]); - friendlyName += (i == 0 ? typeParamName : "," + typeParamName); - } - friendlyName += ">"; - } - - return friendlyName; + return Name; } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.Implementations.cs b/Robust.Shared/Toolshed/ToolshedCommand.Implementations.cs index 4174b5051..d55d1e01e 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.Implementations.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.Implementations.cs @@ -3,136 +3,37 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; -using System.Security; -using Robust.Shared.Log; using Robust.Shared.Utility; namespace Robust.Shared.Toolshed; public abstract partial class ToolshedCommand { - private readonly Dictionary _implementors = new(); - private readonly Dictionary<(CommandDiscriminator, string?), List> _concreteImplementations = new(); + public const BindingFlags MethodFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | + BindingFlags.Instance; - public bool TryGetReturnType(string? subCommand, Type? pipedType, Type[] typeArguments, + public bool TryGetReturnType( + string? subCommand, + Type? pipedType, + Type[]? typeArguments, [NotNullWhen(true)] out Type? type) { - var impls = GetConcreteImplementations(pipedType, typeArguments, subCommand).ToList(); - - if (impls.Count > 0) - { - type = impls.First().ReturnType; - return true; - } - type = null; - return false; + + if (!CommandImplementors.TryGetValue(subCommand ?? string.Empty, out var impl)) + return false; + + if (!impl.TryGetConcreteMethod(pipedType, typeArguments, out var method)) + return false; + + type = method.Value.Info.ReturnType; + return true; } - /// - /// Does its best to find an implementation that can deal with the given types - /// - internal List GetConcreteImplementations(Type? pipedType, Type[] typeArguments, - string? subCommand) + internal IEnumerable GetGenericImplementations() { - var idx = (new CommandDiscriminator(pipedType, typeArguments), subCommand); - if (_concreteImplementations.TryGetValue(idx, - out var impl)) - { - return impl; - } - - impl = GetConcreteImplementationsInternal(pipedType, typeArguments, subCommand); - - _concreteImplementations[idx] = impl; - return impl; - } - - private List GetConcreteImplementationsInternal(Type? pipedType, Type[] typeArguments, - string? subCommand) - { - var impls = GetGenericImplementations() - .Where(x => x.GetCustomAttribute()?.SubCommand == subCommand) - .Where(x => - { - if (x.ConsoleGetPipedArgument() is { } param) - { - return pipedType?.IsAssignableToGeneric(param.ParameterType, Toolshed) ?? false; - } - - return pipedType is null; - }) - .OrderByDescending(x => - { - if (x.ConsoleGetPipedArgument() is { } param) - { - if (pipedType!.IsAssignableTo(param.ParameterType)) - return 1000; // We want exact match to be preferred! - - if (param.ParameterType.GetMostGenericPossible() == pipedType!.GetMostGenericPossible()) - return 500; // If not, try to prefer the same base type. - - // Finally, prefer specialized (type exact) implementations. - return param.ParameterType.IsGenericTypeParameter ? 0 : 100; - } - - return 0; - }) - .Where(x => - { - if (x.IsGenericMethodDefinition) - { - var expectedLen = x.GetGenericArguments().Length; - if (x.HasCustomAttribute()) - expectedLen -= 1; - - return typeArguments.Length == expectedLen; - } - - return typeArguments.Length == 0; - }) - .Select(x => - { - try - { - if (x.IsGenericMethodDefinition) - { - if (x.HasCustomAttribute()) - { - var paramT = x.ConsoleGetPipedArgument()!.ParameterType; - var t = pipedType!.IntersectWithGeneric(paramT, Toolshed, true); - return x.MakeGenericMethod([.. typeArguments, .. t!]); - } - else - return x.MakeGenericMethod(typeArguments); - } - - return x; - } - catch (ArgumentException) - { - // oopsy, toolshed guessed wrong somewhere. Likely due to lack of constraint solver. - return null; - } - - }); - - return impls.Where(x => x is not null).Cast().ToList(); - } - - internal List GetGenericImplementations() - { - var t = GetType(); - - var methods = t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly | - BindingFlags.Instance); - - return methods.Where(x => x.HasCustomAttribute()).ToList(); - } - - internal bool TryGetImplementation(Type? pipedType, string? subCommand, Type[] typeArguments, - [NotNullWhen(true)] out Func? impl) - { - return _implementors[subCommand ?? ""].TryGetImplementation(pipedType, typeArguments, out impl); + return GetType() + .GetMethods(MethodFlags) + .Where(x => x.HasCustomAttribute()); } } diff --git a/Robust.Shared/Toolshed/ToolshedCommand.cs b/Robust.Shared/Toolshed/ToolshedCommand.cs index 5185843d8..2304ec2ac 100644 --- a/Robust.Shared/Toolshed/ToolshedCommand.cs +++ b/Robust.Shared/Toolshed/ToolshedCommand.cs @@ -1,15 +1,12 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; using System.Linq; using System.Reflection; -using System.Threading.Tasks; -using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Reflection; -using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; @@ -55,132 +52,196 @@ public abstract partial class ToolshedCommand /// The user-facing name of the command. /// /// This is automatically generated based on the type name unless overridden with . - public string Name { get; } + public string Name { get; private set; } = default!; /// /// Whether or not this command has subcommands. /// - public bool HasSubCommands { get; } + public bool HasSubCommands; /// /// The additional type parameters of this command, specifically which parsers to use. /// - /// Every type specified must either be itself or something implementing where T is Type. + /// Every type specified must be either be or must inherit from CustomTypeParser<Type>. public virtual Type[] TypeParameterParsers => Array.Empty(); - internal bool HasTypeParameters => TypeParameterParsers.Length != 0; - /// - /// The list of all subcommands on this command. + /// The set of all subcommands on this command. /// - public IEnumerable Subcommands => _implementors.Keys.Where(x => x != ""); + public IEnumerable Subcommands => CommandImplementors.Keys; - /// - /// List of parameters for this command and all sub commands. Used for command help usage. - /// Dictionary(subCommand, List(pipedType, List(parameterType))) - /// - private readonly Dictionary)>> _readonlyParameters; + internal readonly Dictionary CommandImplementors = new(); - protected ToolshedCommand() + private readonly Dictionary> _acceptedTypes = new(); + + protected internal ToolshedCommand() { - var name = GetType().GetCustomAttribute()!.Name; + } + internal void Init() + { + var type = GetType(); + var name = type.GetCustomAttribute()!.Name; if (name is null) { - var typeName = GetType().Name; + var typeName = type.Name; const string commandStr = "Command"; - if (!typeName.EndsWith(commandStr)) - { - throw new InvalidComponentNameException($"Command {GetType()} must end with the word Command"); - } + throw new InvalidCommandImplementation($"Command {type} must end with the word Command"); name = typeName[..^commandStr.Length].ToLowerInvariant(); } + if (string.IsNullOrEmpty(name) || !name.EnumerateRunes().All(ParserContext.IsCommandToken)) + throw new InvalidCommandImplementation($"Command name contains invalid tokens"); + Name = name; - HasSubCommands = false; - _implementors[""] = - new ToolshedCommandImplementor - { - Owner = this, - SubCommand = null - }; - var impls = GetGenericImplementations(); - Dictionary<(string, Type?), SortedDictionary> parameters = new(); + foreach (var typeParser in TypeParameterParsers) + { + if (typeParser == typeof(TypeTypeParser)) + continue; + if (!typeParser.IsAssignableTo(typeof(CustomTypeParser))) + throw new InvalidCommandImplementation($"{nameof(TypeParameterParsers)} element {typeParser} is not {nameof(TypeTypeParser)} or assignable to {typeof(CustomTypeParser).PrettyName()}"); + } - _readonlyParameters = new(); + var impls = GetGenericImplementations().ToArray(); + if (impls.Length == 0) + throw new Exception($"Command has no implementations?"); + + var implementations = new HashSet<(string?, Type?)>(); + var argNames = new HashSet(); + var hasNonSubCommands = false; foreach (var impl in impls) { - var myParams = new SortedDictionary(); - var orderedParams = new List(); + var hasInverted = false; + var hasCtx = false; + Type? pipeType = null; + argNames.Clear(); + + foreach (var param in impl.GetParameters()) + { + var hasAnyAttribute = false; + + if (param.HasCustomAttribute()) + { + if (param.Name == null || !argNames.Add(param.Name)) + throw new InvalidCommandImplementation($"Command arguments must have a unique name"); + hasAnyAttribute = true; + } + + if (param.HasCustomAttribute()) + { + if (hasAnyAttribute) + throw new InvalidCommandImplementation($"Method parameter cannot have more than one relevant attribute"); + if (pipeType != null) + throw new InvalidCommandImplementation($"Commands cannot have more than one piped argument"); + pipeType = param.ParameterType; + hasAnyAttribute = true; + } + + if (param.HasCustomAttribute()) + { + if (hasAnyAttribute) + throw new InvalidCommandImplementation($"Method parameter cannot have more than one relevant attribute"); + if (hasInverted) + throw new InvalidCommandImplementation($"Duplicate {nameof(CommandInvertedAttribute)}"); + if (param.ParameterType != typeof(bool)) + throw new InvalidCommandImplementation($"Command argument with the {nameof(CommandInvertedAttribute)} must be of type bool"); + hasInverted = true; + hasAnyAttribute = true; + } + + if (param.HasCustomAttribute()) + { + if (hasAnyAttribute) + throw new InvalidCommandImplementation($"Method parameter cannot have more than one relevant attribute"); + if (hasCtx) + throw new InvalidCommandImplementation($"Duplicate {nameof(CommandInvocationContextAttribute)}"); + if (param.ParameterType != typeof(IInvocationContext)) + throw new InvalidCommandImplementation($"Command argument with the {nameof(CommandInvocationContextAttribute)} must be of type {nameof(IInvocationContext)}"); + hasCtx = true; + hasAnyAttribute = true; + } + + if (hasAnyAttribute) + continue; + + // Implicit [CommandInvocationContext] + if (param.ParameterType == typeof(IInvocationContext)) + { + if (hasCtx) + throw new InvalidCommandImplementation($"Duplicate (implicit?) {nameof(CommandInvocationContextAttribute)}"); + hasCtx = true; + continue; + } + + // Implicit [CommandArgument] + if (param.Name == null || !argNames.Add(param.Name)) + throw new InvalidCommandImplementation($"Command arguments must have a unique name"); + } + + var takesPipedGeneric = impl.HasCustomAttribute(); + var expected = TypeParameterParsers.Length + (takesPipedGeneric ? 1 : 0); + var genericCount = impl.IsGenericMethodDefinition ? impl.GetGenericArguments().Length : 0; + if (genericCount != expected) + throw new InvalidCommandImplementation("Incorrect number of generic arguments."); + + if (takesPipedGeneric) + { + if (!impl.IsGenericMethodDefinition) + throw new InvalidCommandImplementation($"{nameof(TakesPipedTypeAsGenericAttribute)} requires a method to have generics"); + if (pipeType == null) + throw new InvalidCommandImplementation($"{nameof(TakesPipedTypeAsGenericAttribute)} required there to be a piped parameter"); + + // type that would used to create a concrete method if the desired pipe type were passed in. + var expectedGeneric = ToolshedCommandImplementor.GetGenericTypeFromPiped(pipeType, pipeType); + var lastGeneric = impl.GetGenericArguments()[^1]; + if (expectedGeneric != lastGeneric) + throw new InvalidCommandImplementation($"Commands using {nameof(TakesPipedTypeAsGenericAttribute)} must have the inferred piped parameter type {expectedGeneric.Name} be the last generic parameter"); + } + string? subCmd = null; if (impl.GetCustomAttribute() is {SubCommand: { } x}) { subCmd = x; HasSubCommands = true; - _implementors[x] = - new ToolshedCommandImplementor - { - Owner = this, - SubCommand = x - }; + if (string.IsNullOrEmpty(subCmd) || !subCmd.EnumerateRunes().All(ParserContext.IsToken)) + throw new InvalidCommandImplementation($"Subcommand name {subCmd} contains invalid tokens"); } - - Type? pipedType = null; - foreach (var param in impl.GetParameters()) + else { - if (param.GetCustomAttribute() is not null) - { - if (myParams.TryAdd(param.Name!, param.ParameterType)) - orderedParams.Add(param.ParameterType); - } - - if (param.GetCustomAttribute() is not null) - { - if (pipedType != null) - throw new NotSupportedException($"Commands cannot have more than one piped argument"); - pipedType = param.ParameterType; - } + hasNonSubCommands = true; } - var key = (subCmd ?? "", pipedType); - if (parameters.TryAdd(key, myParams)) - { - var readParam = _readonlyParameters.GetOrNew(subCmd ?? ""); - readParam.Add((pipedType, orderedParams)); - continue; - } + // Currently a command either has no subcommands, or **only** subcommands. This was the behaviour when I got + // here, and I don't see a clear reason why it couldn't be supported if desired. + if (hasNonSubCommands && HasSubCommands) + throw new InvalidCommandImplementation("Toolshed commands either need to be all sub-commands, or have no sub commands at all."); - if (!parameters[key].SequenceEqual(myParams)) - throw new NotImplementedException("All command implementations of a given subcommand with the same pipe type must share the same argument types"); + // AFAIK this is currently just not supported, though it could eventually be added? + if (!implementations.Add((subCmd, pipeType))) + throw new InvalidCommandImplementation("The combination of subcommand and piped parameter type must be unique"); + + var key = subCmd ?? string.Empty; + if (!CommandImplementors.ContainsKey(key)) + CommandImplementors[key] = new ToolshedCommandImplementor(subCmd, this, Toolshed, Loc); } } - internal IEnumerable AcceptedTypes(string? subCommand) + internal HashSet AcceptedTypes(string? subCommand) { - return GetGenericImplementations() - .Where(x => - x.ConsoleGetPipedArgument() is not null - && x.GetCustomAttribute()?.SubCommand == subCommand) - .Select(x => x.ConsoleGetPipedArgument()!.ParameterType); - } + if (_acceptedTypes.TryGetValue(subCommand ?? "", out var set)) + return set; - internal bool TryParseArguments( - bool doAutocomplete, - ParserContext parserContext, - Type? pipedType, - string? subCommand, - [NotNullWhen(true)] out Dictionary? args, - out Type[] resolvedTypeArguments, - out IConError? error, - out ValueTask<(CompletionResult?, IConError?)>? autocomplete - ) - { - - return _implementors[subCommand ?? ""].TryParseArguments(doAutocomplete, parserContext, subCommand, pipedType, out args, out resolvedTypeArguments, out error, out autocomplete); + return _acceptedTypes[subCommand ?? ""] = GetType() + .GetMethods(MethodFlags) + .Where(x => x.GetCustomAttribute() is {} attr && attr.SubCommand == subCommand ) + .Select(x => x.ConsoleGetPipedArgument()) + .Where(x => x != null) + .Select(x => x!.ParameterType) + .ToHashSet(); } } @@ -189,30 +250,73 @@ internal sealed class CommandInvocationArguments public required object? PipedArgument; public required IInvocationContext Context { get; set; } public required CommandArgumentBundle Bundle; - public Dictionary Arguments => Bundle.Arguments; + public Dictionary? Arguments => Bundle.Arguments; public bool Inverted => Bundle.Inverted; - public Type? PipedArgumentType => Bundle.PipedArgumentType; } -internal sealed class CommandArgumentBundle +/// +/// Collection of values used in the process of parsing a single command. +/// +public struct CommandArgumentBundle { - public required Dictionary Arguments; - public required bool Inverted = false; - public required Type? PipedArgumentType; - public required Type[] TypeArguments; + /// + /// The name of the command currently being parsed. + /// + public string? Command; + + /// + /// The name of the sub-command currently being parsed. + /// + public string? SubCommand; + + /// + /// The collection of arguments that will be handed to the command method. + /// + public Dictionary? Arguments; + + /// + /// The collection of type arguments that will be used to get a concrete method for generic commands. + /// This does not include any generic parameters that are inferred from the . + /// + public Type[]? TypeArguments; + + /// + /// The value that will get passed to any method arguments with the . + /// + public required bool Inverted; + + /// + /// The type of input that will be piped into this command. + /// + public required Type? PipedType; } -internal readonly record struct CommandDiscriminator(Type? PipedType, Type[] TypeArguments) +internal readonly record struct CommandDiscriminator(Type? PipedType, Type[]? TypeArguments) { public bool Equals(CommandDiscriminator other) { - return other.PipedType == PipedType && other.TypeArguments.SequenceEqual(TypeArguments); + if (other.PipedType != PipedType) + return false; + + if (other.TypeArguments == null && TypeArguments == null) + return true; + + if (TypeArguments == null) + return false; + + if (TypeArguments.Length != other.TypeArguments!.Length) + return false; + + return TypeArguments.SequenceEqual(other.TypeArguments); } public override int GetHashCode() { // poor man's hash do not judge var h = PipedType?.GetHashCode() ?? (int.MaxValue / 3); + if (TypeArguments == null) + return h; + foreach (var arg in TypeArguments) { h += h ^ arg.GetHashCode(); @@ -222,3 +326,5 @@ internal readonly record struct CommandDiscriminator(Type? PipedType, Type[] Typ return h; } } + +public sealed class InvalidCommandImplementation(string message) : Exception(message); diff --git a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs index 6dd023ab1..274f5059d 100644 --- a/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs +++ b/Robust.Shared/Toolshed/ToolshedCommandImplementor.cs @@ -4,28 +4,96 @@ using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Threading.Tasks; -using Robust.Shared.Console; -using Robust.Shared.IoC; +using System.Text; +using Robust.Shared.Exceptions; +using Robust.Shared.Localization; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Utility; +using Invocable = System.Func; namespace Robust.Shared.Toolshed; internal sealed class ToolshedCommandImplementor { - [Dependency] private readonly ToolshedManager _toolshedManager = default!; - public required ToolshedCommand Owner; + public readonly ToolshedCommand Owner; + public readonly string? SubCommand; - public required string? SubCommand; + public readonly string FullName; - public Dictionary> Implementations = new(); + /// + /// The full name of a command for use when fetching localized strings. + /// + public readonly string LocName; - public ToolshedCommandImplementor() + private readonly ToolshedManager _toolshed; + private readonly ILocalizationManager _loc; + public readonly Dictionary> Implementations = new(); + + /// + /// Cache for . + /// + private readonly Dictionary _methodCache = new(); + + /// + /// All methods in that correspond to the given . + /// + internal readonly CommandMethod[] Methods; + + public CommandSpec Spec => new(Owner, SubCommand); + + public ToolshedCommandImplementor(string? subCommand, ToolshedCommand owner, ToolshedManager toolshed, ILocalizationManager loc) { - IoCManager.InjectDependencies(this); + Owner = owner; + _loc = loc; + SubCommand = subCommand; + FullName = SubCommand == null ? Owner.Name : $"{Owner.Name}:{SubCommand}"; + _toolshed = toolshed; + + Methods = Owner.GetType() + .GetMethods(ToolshedCommand.MethodFlags) + .Where(x => x.GetCustomAttribute() is { } attr && + attr.SubCommand == SubCommand) + .Select(x => new CommandMethod(x)) + .ToArray(); + + LocName = Owner.Name.All(char.IsAsciiLetterOrDigit) + ? Owner.Name + : Owner.GetType().PrettyName(); + + if (SubCommand != null) + LocName = $"{LocName}-{SubCommand}"; + } + + /// + /// Attempt to parse the type-arguments and arguments and return an invocable expression. + /// + public bool TryParse(ParserContext ctx, out Invocable? invocable, [NotNullWhen(true)] out ConcreteCommandMethod? method) + { + ctx.ConsumeWhitespace(); + method = null; + invocable = null; + + if (!TryParseTypeArguments(ctx)) + return false; + + if (!TryGetConcreteMethod(ctx.Bundle.PipedType, ctx.Bundle.TypeArguments, out method)) + { + if (!ctx.GenerateCompletions) + ctx.Error = new NoImplementationError(ctx); + return false; + } + + var argsStart = ctx.Index; + if (!TryParseArguments(ctx, method.Value)) + { + ctx.Error?.Contextualize(ctx.Input, (argsStart, ctx.Index)); + return false; + } + + invocable = GetImplementation(ctx.Bundle, method.Value); + return true; } /// @@ -34,227 +102,543 @@ internal sealed class ToolshedCommandImplementor /// It brings fear to all who tread within, terror to the homes ahead. /// Begone, foul maintainer, for this place is not for thee. /// - public bool TryParseArguments( - bool doAutocomplete, - ParserContext parserContext, - string? subCommand, - Type? pipedType, - [NotNullWhen(true)] out Dictionary? args, - out Type[] resolvedTypeArguments, - out IConError? error, - out ValueTask<(CompletionResult?, IConError?)>? autocomplete - ) + public bool TryParseArguments(ParserContext ctx, ConcreteCommandMethod method) { - resolvedTypeArguments = new Type[Owner.TypeParameterParsers.Length]; + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); - var firstStart = parserContext.Index; - - // HACK: This is for commands like Map until I have a better solution. - if (Owner.GetType().GetCustomAttribute() is {} mapLike) + ref var args = ref ctx.Bundle.Arguments; + foreach (var arg in method.Args) { - var start = parserContext.Index; - // We do our own parsing, assuming this is some kind of map-like operation. - var chkpoint = parserContext.Save(); - if (!Block.TryParse(doAutocomplete, parserContext, mapLike.TakesPipedType ? pipedType!.GetGenericArguments()[0] : null, out var block, out autocomplete, out error)) - { - error?.Contextualize(parserContext.Input, (start, parserContext.Index)); - resolvedTypeArguments = Array.Empty(); - args = null; + if (!TryParseArgument(ctx, arg, ref args)) return false; - } - - resolvedTypeArguments[0] = block.CommandRun.Commands.Last().Item1.ReturnType!; - parserContext.Restore(chkpoint); - goto mapLikeDone; } - for (var i = 0; i < Owner.TypeParameterParsers.Length; i++) + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + return true; + } + + private bool TryParseArgument(ParserContext ctx, CommandArgument arg, ref Dictionary? args) + { + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + var start = ctx.Index; + var save = ctx.Save(); + ctx.ConsumeWhitespace(); + + if (ctx.PeekCommandOrBlockTerminated() || ctx is {OutOfInput: true, GenerateCompletions: false}) { - var start = parserContext.Index; - var chkpoint = parserContext.Save(); - if (!_toolshedManager.TryParse(parserContext, Owner.TypeParameterParsers[i], out var parsed, out error) || parsed is not { } ty) - { - error?.Contextualize(parserContext.Input, (start, parserContext.Index)); - resolvedTypeArguments = Array.Empty(); - args = null; - autocomplete = null; - if (doAutocomplete) - { - parserContext.Restore(chkpoint); - autocomplete = _toolshedManager.TryAutocomplete(parserContext, Owner.TypeParameterParsers[i], null); - } - - return false; - } - - Type real; - if (ty is IAsType asTy) - { - real = asTy.AsType(); - } - else if (ty is Type realTy) - { - real = realTy; - } - else - { - throw new NotImplementedException(); - } - - resolvedTypeArguments[i] = real; - } - - mapLikeDone: - var impls = Owner.GetConcreteImplementations(pipedType, resolvedTypeArguments, subCommand); - if (impls.FirstOrDefault() is not { } impl) - { - args = null; - error = new NoImplementationError(Owner.Name, resolvedTypeArguments, subCommand, pipedType, parserContext.Environment); - error.Contextualize(parserContext.Input, (firstStart, parserContext.Index)); - autocomplete = null; + ctx.Error = new ExpectedArgumentError(arg.Type); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); return false; } - autocomplete = null; - args = new(); - foreach (var argument in impl.ConsoleGetArguments()) + if (!arg.Parser.TryParse(ctx, out var parsed)) { - var start = parserContext.Index; - var chkpoint = parserContext.Save(); - if (!_toolshedManager.TryParse(parserContext, argument.ParameterType, out var parsed, out error)) + if (ctx.GenerateCompletions) { - error?.Contextualize(parserContext.Input, (start, parserContext.Index)); - args = null; + // Dont generate completions for the end of a command for an error that occured early on. + // I.e., "i asd " should not be suggesting people to enter an integer. + if (!ctx.OutOfInput) + return false; - // Only generate auto-completions if the parsing error happened for the last argument. - if (doAutocomplete && parserContext.Index > parserContext.MaxIndex) - { - parserContext.Restore(chkpoint); - autocomplete = _toolshedManager.TryAutocomplete(parserContext, argument.ParameterType, null); - } + // Some parsers might already generate completions when they fail the initial parsing. + if (ctx.Completions != null) + return false; + + ctx.Restore(save); + ctx.Error = null; + ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg.Name); + TrySetArgHint(ctx, arg.Name); return false; } - args[argument.Name!] = parsed; - if (!doAutocomplete || parserContext.Index <= parserContext.MaxIndex) - continue; + // un-parseable types don't even consume input / modify the index + // However for contextualizing the error, we want to at least show where the failing argument was + var end = Math.Max(start + 1, ctx.Index); - // This was the end of the input, so we want to get completions for the current argument, not the next argument. - doAutocomplete = false; - var chkpoint2 = parserContext.Save(); - parserContext.Restore(chkpoint); - autocomplete = _toolshedManager.TryAutocomplete(parserContext, argument.ParameterType, null); - parserContext.Restore(chkpoint2); + ctx.Error ??= new ArgumentParseError(arg.Type, arg.Parser.GetType()); + ctx.Error.Contextualize(ctx.Input, (start, end)); + return false; } - error = null; + // All arguments should have been parsed as a ValueRef or Block, unless this is using some custom type parser +#if DEBUG + var t = parsed.GetType(); + if (arg.Parser.GetType().IsCustomParser()) + { + DebugTools.Assert(t.IsAssignableTo(arg.Type) + || t.IsAssignableTo(typeof(Block)) + || t.IsValueRef()); + } + else if (arg.Type.IsAssignableTo(typeof(Block))) + DebugTools.Assert(t.IsAssignableTo(typeof(Block))); + else + DebugTools.Assert(t.IsValueRef()); +#endif + + args ??= new(); + args[arg.Name] = parsed; + + if (!ctx.GenerateCompletions || !ctx.OutOfInput) + return true; + + // This was the end of the input, so we want to get completions for the current argument, not the next + // argument. I.e., if we started writing out a variable name, we want to keep generating variable name + // suggestions for the current argument. This is true even if the current string corresponds to a valid + // variable. + + ctx.Restore(save); + ctx.Error = null; + ctx.Completions ??= arg.Parser.TryAutocomplete(ctx, arg.Name); + TrySetArgHint(ctx, arg.Name); + + // TODO TOOLSHED invalid-fail + // This can technically "fail" to parse a valid command, however this only happens when generating + // completions, not when actually executing the command. Still, this is pretty janky and I don't know of a + // good fix. + return false; + } + + + private void TrySetArgHint(ParserContext ctx, string argName) + { + if (ctx.Completions == null) + return; + + if (_loc.TryGetString($"command-arg-hint-{LocName}-{argName}", out var hint)) + ctx.Completions.Hint = hint; + } + + internal bool TryParseTypeArguments(ParserContext ctx) + { + if (Owner.TypeParameterParsers.Length == 0) + return true; + + ref var typeArguments = ref ctx.Bundle.TypeArguments; + typeArguments = new Type[Owner.TypeParameterParsers.Length]; + + for (var i = 0; i < Owner.TypeParameterParsers.Length; i++) + { + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + var parserType = Owner.TypeParameterParsers[i]; + var start = ctx.Index; + ctx.ConsumeWhitespace(); + var save = ctx.Save(); + + if (ctx is {OutOfInput: true, GenerateCompletions: false} || ctx.PeekCommandOrBlockTerminated()) + { + ctx.Error = new ExpectedTypeArgumentError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); + return false; + } + + var parser = (BaseParser) (parserType == typeof(TypeTypeParser) + ? _toolshed.GetParserForType(typeof(Type))! + : _toolshed.GetCustomParser(parserType)); + + DebugTools.AssertNull(ctx.Completions); + if (!parser.TryParse(ctx, out var parsed)) + { + typeArguments = null; + if (ctx.GenerateCompletions) + { + // Dont generate completions for the end of a command for an error that occured early on. + // I.e., "i asd " should not be suggesting people to enter an integer. + if (!ctx.OutOfInput) + return false; + + ctx.Restore(save); + ctx.Error = null; + ctx.Completions ??= parser.TryAutocomplete(ctx, null); + return false; + } + + ctx.Error ??= new TypeArgumentParseError(parserType); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + + typeArguments[i] = parsed; + + if (!ctx.GenerateCompletions || !ctx.OutOfInput) + continue; + + // This was the end of the input, so we want to get completions for the current argument, not the next + // argument. I.e., if we started writing out the name of a type, we want to keep generating type name + // suggestions for the current argument. This is true even if the current string already corresponds to a + // valid type. + + ctx.Restore(save); + ctx.Error = null; + ctx.Completions = parser.TryAutocomplete(ctx, null); + + // TODO TOOLSHED invalid-fail + // This can technically "fail" to parse a valid command, however this only happens when generating + // completions, not when actually executing the command. Still, this is pretty janky and I don't know of a + // good fix. + return false; + } + + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); return true; } /// - /// Attempts to generate a callable shim for a command, aka it's implementation, using the given types. + /// Attempt to get a concrete method that takes in the given generic type arguments. /// - public bool TryGetImplementation(Type? pipedType, Type[] typeArguments, [NotNullWhen(true)] out Func? impl) + internal bool TryGetConcreteMethod( + Type? pipedType, + Type[]? typeArguments, + [NotNullWhen(true)] out ConcreteCommandMethod? method) { - var discrim = new CommandDiscriminator(pipedType, typeArguments); + var idx = new CommandDiscriminator(pipedType, typeArguments); + if (_methodCache.TryGetValue(idx, out method)) + return method != null; - if (Implementations.TryGetValue(discrim, out impl)) + var result = GetConcreteMethodInternal(pipedType, typeArguments); + if (result == null) + { + _methodCache[idx] = method = null; + return false; + } + + var (cmd, info) = result.Value; + if (pipedType is {ContainsGenericParameters: true} || typeArguments != null && typeArguments.Any(x => x.ContainsGenericParameters)) + { + // I hate this method name + // its not a real concrete method if the requested types are generic, is it now? + // anyways, fuck this I CBF fixing it just return without information about the arguments. + _methodCache[idx] = method = new(info, default!, cmd); return true; - - if (!Owner.TryGetReturnType(SubCommand, pipedType, typeArguments, out var ty)) - { - impl = null; - return false; } - // Okay we need to build a new shim. + var args = info.GetParameters() + .Where(x => x.IsCommandArgument()) + .Select(x => new CommandArgument(x.Name!, x.ParameterType, GetArgumentParser(x))) + .ToArray(); - var possibleImpls = Owner.GetConcreteImplementations(pipedType, typeArguments, SubCommand); - - IEnumerable impls; - - if (pipedType is null) - { - impls = possibleImpls.Where(x => - x.ConsoleGetPipedArgument() is {} param && param.ParameterType.CanBeEmpty() - || x.ConsoleGetPipedArgument() is null - || x.GetParameters().Length == 0); - } - else - { - impls = possibleImpls.Where(x => - x.ConsoleGetPipedArgument() is {} param && _toolshedManager.IsTransformableTo(pipedType, param.ParameterType) - || x.IsGenericMethodDefinition); - } - - var implArray = impls.ToArray(); - if (implArray.Length == 0) - { - return false; - } - - var unshimmed = implArray.First(); - - var args = Expression.Parameter(typeof(CommandInvocationArguments)); - - var paramList = new List(); - - foreach (var param in unshimmed.GetParameters()) - { - if (param.GetCustomAttribute() is { } _) - { - if (pipedType is null) - { - paramList.Add(param.ParameterType.CreateEmptyExpr()); - } - else - { - // (ParameterType)(args.PipedArgument) - paramList.Add(_toolshedManager.GetTransformer(pipedType, param.ParameterType, Expression.Field(args, nameof(CommandInvocationArguments.PipedArgument)))); - } - - continue; - } - - if (param.GetCustomAttribute() is { } arg) - { - // (ParameterType)(args.Arguments[param.Name]) - paramList.Add(Expression.Convert( - Expression.MakeIndex( - Expression.Property(args, nameof(CommandInvocationArguments.Arguments)), - typeof(Dictionary).FindIndexerProperty(), - new [] {Expression.Constant(param.Name)}), - param.ParameterType)); - continue; - } - - if (param.GetCustomAttribute() is { } _) - { - // args.Inverted - paramList.Add(Expression.Property(args, nameof(CommandInvocationArguments.Inverted))); - continue; - } - - if (param.GetCustomAttribute() is { } _) - { - // args.Context - paramList.Add(Expression.Property(args, nameof(CommandInvocationArguments.Context))); - continue; - } - - } - - Expression partialShim = Expression.Call(Expression.Constant(Owner), unshimmed, paramList); - - if (unshimmed.ReturnType == typeof(void)) - partialShim = Expression.Block(partialShim, Expression.Constant(null)); - else if (ty is not null && ty.IsValueType) - partialShim = Expression.Convert(partialShim, typeof(object)); // Have to box primitives. - - var lambda = Expression.Lambda>(partialShim, args); - - Implementations[discrim] = lambda.Compile(); - impl = Implementations[discrim]; + _methodCache[idx] = method = new(info, args, cmd); return true; } + + private ITypeParser GetArgumentParser(ParameterInfo param) + { + var attrib = param.GetCustomAttribute(); + var parser = attrib?.CustomParser is not {} custom + ? _toolshed.GetArgumentParser(param.ParameterType) + : _toolshed.GetArgumentParser(_toolshed.GetCustomParser(custom)); + + if (parser == null) + throw new Exception($"No parser for type: {param.ParameterType}"); + return parser; + } + + private (CommandMethod, MethodInfo)? GetConcreteMethodInternal(Type? pipedType, Type[]? typeArguments) + { + return Methods + .Where(x => + { + if (x.PipeArg is not { } param) + return pipedType is null; + + if (pipedType == null) + return false; // We want exact match to be preferred! + + return x.Generic || _toolshed.IsTransformableTo(pipedType, param.ParameterType); + + // Finally, prefer specialized (type exact) implementations. + }) + .OrderByDescending(x => + { + if (x.PipeArg is not { } param) + return 0; + + if (pipedType!.IsAssignableTo(param.ParameterType)) + return 1000; // We want exact match to be preferred! + if (param.ParameterType.GetMostGenericPossible() == pipedType.GetMostGenericPossible()) + return 500; // If not, try to prefer the same base type. + + // Finally, prefer specialized (type exact) implementations. + return param.ParameterType.IsGenericTypeParameter ? 0 : 100; + + }) + .Select(x => + { + if (!x.Generic) + return ((CommandMethod, MethodInfo)?)(x, x.Info); + + try + { + if (!x.PipeGeneric) + return (x, x.Info.MakeGenericMethod(typeArguments!)); + + var t = GetGenericTypeFromPiped(pipedType!, x.PipeArg!.ParameterType); + return (x, x.Info.MakeGenericMethod(typeArguments?.Append(t).ToArray() ?? [t])); + } + catch (ArgumentException) + { + return null; + } + }) + .FirstOrDefault(x => x != null); + } + + /// + /// When a method has the , this method is used to actually + /// determine the generic type argument given the type of the piped in value. + /// + /// The type of value that was piped in + /// The type as specified in the method + public static Type GetGenericTypeFromPiped(Type inputType, Type parameterType) + { + // inputType!.IntersectWithGeneric(parameterType, _toolshed, true); + + // I don't really understand the logic behind this + // Actually I understand it now, but its just broken or incomplete. Yipeee + return inputType.Intersect(parameterType); +} + + /// + /// Attempts to fetch a callable shim for a command, aka it's implementation, using the given types. + /// + public Func GetImplementation(CommandArgumentBundle args, ConcreteCommandMethod method) + { + var dis = new CommandDiscriminator(args.PipedType, args.TypeArguments); + if (!Implementations.TryGetValue(dis, out var impl)) + Implementations[dis] = impl = GetImplementationInternal(args, method); + + return impl; + } + + internal Func GetImplementationInternal(CommandArgumentBundle cmdArgs, ConcreteCommandMethod method) + { + var args = Expression.Parameter(typeof(CommandInvocationArguments)); + var paramList = new List(); + + foreach (var param in method.Info.GetParameters()) + { + paramList.Add(GetParamExpr(param, cmdArgs.PipedType, args)); + } + + Expression partialShim = Expression.Call(Expression.Constant(Owner), method.Info, paramList); + + var returnType = method.Info.ReturnType; + if (returnType == typeof(void)) + partialShim = Expression.Block(partialShim, Expression.Constant(null)); + else if (returnType.IsValueType) + partialShim = Expression.Convert(partialShim, typeof(object)); // Have to box primitives. + + return Expression.Lambda>(partialShim, args).Compile(); + } + + private Expression GetParamExpr(ParameterInfo param, Type? pipedType, ParameterExpression args) + { + if (param.HasCustomAttribute()) + { + if (pipedType is null) + throw new TypeArgumentException(); + + // (ParameterType)(args.PipedArgument) + return _toolshed.GetTransformer(pipedType, param.ParameterType, Expression.Field(args, nameof(CommandInvocationArguments.PipedArgument))); + } + + if (param.HasCustomAttribute()) + { + // args.Inverted + return Expression.Property(args, nameof(CommandInvocationArguments.Inverted)); + } + + if (param.HasCustomAttribute()) + return GetArgExpr(param, args); + + if (param.HasCustomAttribute() + || param.ParameterType == typeof(IInvocationContext)) + { + // args.Context + return Expression.Property(args, nameof(CommandInvocationArguments.Context)); + } + + // Implicit CommandArgumentAttribute + return GetArgExpr(param, args); + } + + private Expression GetArgExpr(ParameterInfo param, ParameterExpression args) + { + // args.Arguments[param.Name] + var argValue = Expression.MakeIndex( + Expression.Property(args, nameof(CommandInvocationArguments.Arguments)), + typeof(Dictionary).FindIndexerProperty(), + new[] {Expression.Constant(param.Name)}); + + // args.Context + var ctx = Expression.Property(args, nameof(CommandInvocationArguments.Context)); + + // ValueRef.TryEvaluate + var evalMethod = typeof(ValueRef<>) + .MakeGenericType(param.ParameterType) + .GetMethod(nameof(ValueRef.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!; + + // ValueRef.TryEvaluate(args.Arguments[param.Name], args.Context) + return Expression.Call(evalMethod, argValue, ctx); + } + + public override string ToString() + { + return FullName; + } + + /// + public string GetHelp() + { + if (_loc.TryGetString($"command-help-{LocName}", out var str)) + return str; + + var builder = new StringBuilder(); + + // If any of the commands are invertible via the "not" prefix, we point that out in the help string + if (Methods.Any(x => x.Invertible)) + builder.AppendLine(_loc.GetString($"command-help-invertible")); + + // List usages by just printing all methods & their arguments + builder.Append(_loc.GetString("command-help-usage")); + foreach (var method in Methods) + { + builder.Append(Environment.NewLine + " "); + + if (method.PipeArg != null) + builder.Append($"<{method.PipeArg.Name} ({GetFriendlyName(method.PipeArg.ParameterType)})> -> "); + + if (method.Invertible) + builder.Append("[not] "); + + builder.Append(FullName); + + foreach (var (argName, argType) in method.Arguments) + { + builder.Append($" <{argName} ({GetFriendlyName(argType)})>"); + } + + if (method.Info.ReturnType != typeof(void)) + builder.Append($" -> {GetFriendlyName(method.Info.ReturnType)}"); + } + + return builder.ToString(); + } + + /// + public string DescriptionLocKey() => $"command-description-{LocName}"; + + /// + public string Description() + { + return _loc.GetString(DescriptionLocKey()); + } + + public static string GetFriendlyName(Type type) + { + var friendlyName = type.Name; + if (!type.IsGenericType) + return friendlyName; + + var iBacktick = friendlyName.IndexOf('`'); + if (iBacktick > 0) + friendlyName = friendlyName.Remove(iBacktick); + + friendlyName += "<"; + var typeParameters = type.GetGenericArguments(); + for (var i = 0; i < typeParameters.Length; ++i) + { + var typeParamName = GetFriendlyName(typeParameters[i]); + friendlyName += (i == 0 ? typeParamName : "," + typeParamName); + } + friendlyName += ">"; + + return friendlyName; + } +} + + +/// +/// Struct for caching information about a command's methods. Helps reduce LINQ & reflection calls when attempting +/// to find matching methods. +/// +internal sealed class CommandMethod +{ + /// + /// The method associated with some command. + /// + public readonly MethodInfo Info; + + /// + /// The argument associated with the piped value. + /// + public readonly ParameterInfo? PipeArg; + + public readonly bool Generic; + public readonly bool Invertible; + + /// + /// Whether the type of the piped value should be used as one of the type parameters for generic methods. + /// I.e., whether the method has a . + /// + public readonly bool PipeGeneric; + + public readonly (string, Type)[] Arguments; + + public CommandMethod(MethodInfo info) + { + Info = info; + PipeArg = info.ConsoleGetPipedArgument(); + Invertible = info.ConsoleHasInvertedArgument(); + + Arguments = info.GetParameters() + .Where(x => x.IsCommandArgument()) + .Select(x => (x.Name ?? string.Empty, x.ParameterType)) + .ToArray(); + + if (!info.IsGenericMethodDefinition) + return; + + Generic = true; + PipeGeneric = info.HasCustomAttribute(); + } +} + +internal readonly record struct ConcreteCommandMethod(MethodInfo Info, CommandArgument[] Args, CommandMethod Base); +internal readonly record struct CommandArgument(string Name, Type Type, ITypeParser Parser); + +public sealed class ArgumentParseError(Type type, Type parser) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Failed to parse command argument of type {type.PrettyName()} using parser {parser.PrettyName()}"); + } +} + +public sealed class ExpectedArgumentError(Type type) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Expected command argument of type {type.PrettyName()}, but ran out of input"); + } +} + +public sealed class TypeArgumentParseError(Type parser) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Failed to parse type argument using parser {parser.PrettyName()}"); + } +} + +public sealed class ExpectedTypeArgumentError : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Expected type argument, but ran out of input"); + } } diff --git a/Robust.Shared/Toolshed/ToolshedEnvironment.cs b/Robust.Shared/Toolshed/ToolshedEnvironment.cs index b1f998f90..2200c8cd4 100644 --- a/Robust.Shared/Toolshed/ToolshedEnvironment.cs +++ b/Robust.Shared/Toolshed/ToolshedEnvironment.cs @@ -2,11 +2,12 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Robust.Shared.Console; using Robust.Shared.IoC; using Robust.Shared.Log; -using Robust.Shared.Network; using Robust.Shared.Reflection; using Robust.Shared.Timing; +using Robust.Shared.Utility; namespace Robust.Shared.Toolshed; @@ -15,10 +16,19 @@ public sealed class ToolshedEnvironment [Dependency] private readonly IReflectionManager _reflection = default!; [Dependency] private readonly ILogManager _logManager = default!; [Dependency] private readonly ToolshedManager _toolshedManager = default!; + [Dependency] private readonly IDependencyCollection _dependency = default!; + + // Dictionary of commands, not including sub-commands private readonly Dictionary _commands = new(); + + // All commands, including subcommands. + private List _allCommands = new(); + + private readonly Dictionary> _commandTypeMap = new(); private readonly Dictionary> _commandPipeValueMap = new(); private readonly Dictionary> _commandReturnValueMap = new(); - + private readonly Dictionary _commandTypeCache = new(); + private readonly Dictionary _commandCompletionCache = new(); private ISawmill _log = default!; @@ -26,22 +36,9 @@ public sealed class ToolshedEnvironment /// Provides every registered command, including subcommands. /// /// Enumerable of every command. - public IEnumerable AllCommands() + public IReadOnlyList AllCommands() { - foreach (var (_, cmd) in _commands) - { - if (cmd.HasSubCommands) - { - foreach (var subcommand in cmd.Subcommands) - { - yield return new(cmd, subcommand); - } - } - else - { - yield return new(cmd, null); - } - } + return _allCommands; } /// @@ -63,40 +60,31 @@ public sealed class ToolshedEnvironment return _commands.TryGetValue(commandName, out command); } - public ToolshedEnvironment(IEnumerable commands) - { - IoCManager.InjectDependencies(this); - - foreach (var ty in commands) - { - if (!ty.IsAssignableTo(typeof(ToolshedCommand))) - { - _log.Error($"The type {ty.AssemblyQualifiedName} was provided in a ToolshedEnvironment's constructor without being a child of {nameof(ToolshedCommand)}"); - continue; - } - - var command = (ToolshedCommand)Activator.CreateInstance(ty)!; - IoCManager.Resolve().InjectDependencies(command, oneOff: true); - - _commands.Add(command.Name, command); - } - - InitializeQueries(); - } - /// /// Initializes a default toolshed context. /// public ToolshedEnvironment() { IoCManager.InjectDependencies(this); + Init(_reflection.FindTypesWithAttribute()); + } + /// + /// Initialized a toolshed context with only the specified toolshed commands. + /// + public ToolshedEnvironment(IEnumerable commands) + { + IoCManager.InjectDependencies(this); + Init(commands); + } + + private void Init(IEnumerable commands) + { _log = _logManager.GetSawmill("toolshed"); var watch = new Stopwatch(); watch.Start(); - var tys = _reflection.FindTypesWithAttribute(); - foreach (var ty in tys) + foreach (var ty in commands) { if (!ty.IsAssignableTo(typeof(ToolshedCommand))) { @@ -104,62 +92,45 @@ public sealed class ToolshedEnvironment continue; } - var command = (ToolshedCommand)Activator.CreateInstance(ty)!; - IoCManager.Resolve().InjectDependencies(command, oneOff: true); + var cmd = (ToolshedCommand)Activator.CreateInstance(ty)!; + _dependency.InjectDependencies(cmd, oneOff: true); + cmd.Init(); + _commands.Add(cmd.Name, cmd); - _commands.Add(command.Name, command); - } + var list = new List(); + _commandTypeMap.Add(ty, list); - InitializeQueries(); - - _log.Info($"Initialized new toolshed context in {watch.Elapsed}"); - } - - private void InitializeQueries() - { - foreach (var (_, cmd) in _commands) - { - foreach (var (subcommand, methods) in cmd.GetGenericImplementations().BySubCommand()) + foreach (var impl in cmd.CommandImplementors.Values) { - foreach (var method in methods) + list.Add(impl.Spec); + _allCommands.Add(impl.Spec); + + foreach (var method in impl.Methods) { - var piped = method.ConsoleGetPipedArgument()?.ParameterType; + var piped = method.PipeArg?.ParameterType ?? typeof(void); - if (piped is null) - piped = typeof(void); - - var list = GetTypeImplList(piped); - var invList = GetCommandRetValuesInternal(new CommandSpec(cmd, subcommand)); - list.Add(new CommandSpec(cmd, subcommand == "" ? null : subcommand)); - if (cmd.TryGetReturnType(subcommand, piped, Array.Empty(), out var retType) || method.ReturnType.Constructable()) + GetTypeImplList(piped).Add(impl.Spec); + var invList = GetCommandRetValuesInternal(impl.Spec); + if (cmd.TryGetReturnType(impl.SubCommand, piped, null, out var retType) || method.Info.ReturnType.Constructable()) { - invList.Add((retType ?? method.ReturnType)); + invList.Add((retType ?? method.Info.ReturnType)); } } } } + _log.Info($"Initialized new toolshed context in {watch.Elapsed}"); } - /// - /// Returns all commands that fit the given type constraints. - /// - /// Enumerable of matching command specs. - public IEnumerable CommandsFittingConstraint(Type input, Type output) + public bool TryGetCommands([NotNullWhen(true)] out IReadOnlyList? commands) + where T : ToolshedCommand { - foreach (var (command, subcommand) in CommandsTakingType(input)) - { - if (command.HasTypeParameters) - continue; // We don't consider these right now. + commands = null; + if (!_commandTypeMap.TryGetValue(typeof(T), out var list)) + return false; - var impls = command.GetConcreteImplementations(input, Array.Empty(), subcommand); - - foreach (var impl in impls) - { - if (impl.ReturnType.IsAssignableTo(output)) - yield return new CommandSpec(command, subcommand); - } - } + commands = list; + return true; } /// @@ -168,23 +139,55 @@ public sealed class ToolshedEnvironment /// Type to use in the query. /// Enumerable of matching command specs. /// Not currently type constraint aware. - public IEnumerable CommandsTakingType(Type t) + internal CommandSpec[] CommandsTakingType(Type? t) { + t ??= typeof(void); + if (_commandTypeCache.TryGetValue(t, out var arr)) + return arr; + var output = new Dictionary<(string, string?), CommandSpec>(); foreach (var type in _toolshedManager.AllSteppedTypes(t)) { var list = GetTypeImplList(type); - if (type.IsGenericType) - { - list = Enumerable.Concat(list, GetTypeImplList(type.GetGenericTypeDefinition())).ToList(); - } foreach (var entry in list) { output.TryAdd((entry.Cmd.Name, entry.SubCommand), entry); } + + if (!type.IsGenericType) + continue; + + foreach (var entry in GetTypeImplList(type.GetGenericTypeDefinition())) + { + output.TryAdd((entry.Cmd.Name, entry.SubCommand), entry); + } } - return output.Values; + return _commandTypeCache[t] = output.Values.ToArray(); + } + + // TODO TOOLSHED Fix CommandCompletionsForType + // This fails to generate some completions. E.g., "i 1 iota iterate". It never generates the completions for + // iterate, even though it takes in an unconstrained generic type. Note that this is just for completions, the + // actual command executes fine. E.g.: "i 1 iota iterate { take 1 } 3" works as spected + public CompletionResult CommandCompletionsForType(Type? t) + { + t ??= typeof(void); + if (!_commandCompletionCache.TryGetValue(t, out var arr)) + arr = _commandCompletionCache[t] = CommandsTakingType(t).Select(x => x.AsCompletion()).ToArray(); + + return CompletionResult.FromHintOptions(arr, ""); + } + + public CompletionResult SubCommandCompletionsForType(Type? t, ToolshedCommand command) + { + // TODO TOOLSHED Cache this? + // Maybe cache this or figure out some way to avoid having to iterate over unrelated commands. + // I.e., restrict the iteration to only happen over subcommands. + var cmds = CommandsTakingType(t) + .Where(x => x.Cmd == command) + .Select(x => x.AsCompletion()); + return CompletionResult.FromHintOptions(cmds, ""); } /// @@ -198,13 +201,7 @@ public sealed class ToolshedEnvironment private HashSet GetCommandRetValuesInternal(CommandSpec command) { - if (!_commandReturnValueMap.TryGetValue(command, out var l)) - { - l = new(); - _commandReturnValueMap[command] = l; - } - - return l; + return _commandReturnValueMap.GetOrNew(command); } private List GetTypeImplList(Type t) @@ -224,17 +221,9 @@ public sealed class ToolshedEnvironment t = t.GetGenericTypeDefinition(); } - if (t.IsGenericType && !t.IsConstructedGenericType) - { + if (t is {IsGenericType: true, IsConstructedGenericType: false}) t = t.GetGenericTypeDefinition(); - } - if (!_commandPipeValueMap.TryGetValue(t, out var l)) - { - l = new(); - _commandPipeValueMap[t] = l; - } - - return l; + return _commandPipeValueMap.GetOrNew(t); } } diff --git a/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs b/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs index 8171749b5..d6a43f1fa 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.Parsing.cs @@ -4,8 +4,8 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Security; -using System.Threading.Tasks; using Robust.Shared.Console; +using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; @@ -17,21 +17,43 @@ namespace Robust.Shared.Toolshed; public sealed partial class ToolshedManager { private readonly Dictionary _consoleTypeParsers = new(); + private readonly Dictionary _argParsers = new(); + private readonly Dictionary _customParsers = new(); private readonly Dictionary _genericTypeParsers = new(); private readonly List<(Type, Type)> _constrainedParsers = new(); private void InitializeParser() { + // This contains both custom parsers, and default type parsers var parsers = _reflection.GetAllChildren(); foreach (var parserType in parsers) { + var parent = parserType.BaseType; + + var found = false; + while (parent != null) + { + if (parent.IsGenericType(typeof(TypeParser<>))) + { + found = true; + break; + } + parent = parent.BaseType; + } + + if (!found) + continue; + if (parserType.IsGenericType) { var t = parserType.BaseType!.GetGenericArguments().First(); if (t.IsGenericType) { - _genericTypeParsers.Add(t.GetGenericTypeDefinition(), parserType); + var key = t.GetGenericTypeDefinition(); + if (!_genericTypeParsers.TryAdd(key, parserType)) + throw new Exception($"Duplicate toolshed type parser for type: {key}"); + _log.Verbose($"Setting up {parserType.PrettyName()}, {t.GetGenericTypeDefinition().PrettyName()}"); } else if (t.IsGenericParameter) @@ -43,26 +65,101 @@ public sealed partial class ToolshedManager else { var parser = (ITypeParser) _typeFactory.CreateInstanceUnchecked(parserType, oneOff: true); - parser.PostInject(); + if (parser is IPostInjectInit inj) + inj.PostInject(); + _log.Verbose($"Setting up {parserType.PrettyName()}, {parser.Parses.PrettyName()}"); - _consoleTypeParsers.Add(parser.Parses, parser); + if (!_consoleTypeParsers.TryAdd(parser.Parses, parser)) + { + throw new Exception($"Discovered conflicting parsers for type {parser.Parses.PrettyName()}: {parserType.PrettyName()} and {_consoleTypeParsers[parser.Parses]!.GetType().PrettyName()}"); + } } } } - private ITypeParser? GetParserForType(Type t) + internal ITypeParser? GetParserForType(Type t) { if (_consoleTypeParsers.TryGetValue(t, out var parser)) return parser; parser = FindParserForType(t); + DebugTools.Assert(parser == null || parser.Parses == t); _consoleTypeParsers.TryAdd(t, parser); return parser; + } + /// + /// Variant of that will return a parser that also attempts to resolve a type from a + /// variable or block via the and parsers. + /// + internal ITypeParser? GetArgumentParser(Type t) + { + var parser = GetParserForType(t); + if (parser != null) + return GetArgumentParser(parser); + + // Some types are not directly parsable, but can still be passes as arguments by using variables or blocks. + DebugTools.Assert(!t.IsValueRef() && !t.IsAssignableTo(typeof(Block))); + return GetParserForType(typeof(ValueRef<>).MakeGenericType(t)); + } + + /// + /// Variant of that will return a parser that also attempts to resolve a type from a + /// variable or block via the parsers. If that fails, it will fall back to using the given + /// type parser + /// + internal ITypeParser? GetArgumentParser(ITypeParser baseParser) + { + if (!baseParser.EnableValueRef) + return baseParser; + + if (_argParsers.TryGetValue(baseParser, out var parser)) + return parser; + + var t = baseParser.Parses; + + if (t.IsValueRef() || t.IsAssignableTo(typeof(Block))) + parser = baseParser; + else if (baseParser.GetType().HasGenericParent(typeof(TypeParser<>))) + parser = GetParserForType(typeof(ValueRef<>).MakeGenericType(t)); + else + parser = GetCustomParser(typeof(CustomValueRefTypeParser<,>).MakeGenericType(t, baseParser.GetType())); + + return _argParsers[baseParser] = parser; + } + + internal TParser GetCustomParser() where TParser : CustomTypeParser, new() where T : notnull + { + return (TParser)GetCustomParser(typeof(TParser)); + } + + /// + /// Attempt to fetch the custom parser instance of the given type. + /// + internal ITypeParser GetCustomParser(Type parser) + { + if (_customParsers.TryGetValue(parser, out var result)) + return result; + + if (parser.ContainsGenericParameters) + throw new ArgumentException($"Type cannot contain generic parameters"); + + if (!parser.IsCustomParser()) + throw new ArgumentException($"{parser.PrettyName()} does not inherit from {typeof(CustomTypeParser<>).PrettyName()}"); + + result = (ITypeParser) _typeFactory.CreateInstanceUnchecked(parser, true); + if (result is IPostInjectInit inj) + inj.PostInject(); + + return _customParsers[parser] = result; } private ITypeParser? FindParserForType(Type t) { + // Accidentally using FindParserForType() instead of GetParserForType() can lead to very fun bugs. + // Hence this assert. + DebugTools.Assert(!_consoleTypeParsers.ContainsKey(t)); + if (t.IsConstructedGenericType) { if (_genericTypeParsers.TryGetValue(t.GetGenericTypeDefinition(), out var genParser)) @@ -70,9 +167,11 @@ public sealed partial class ToolshedManager try { var concreteParser = genParser.MakeGenericType(t.GenericTypeArguments); - var builtParser = (ITypeParser) _typeFactory.CreateInstanceUnchecked(concreteParser, true); - builtParser.PostInject(); + + if (builtParser is IPostInjectInit inj) + inj.PostInject(); + return builtParser; } catch (SecurityException) @@ -84,7 +183,7 @@ public sealed partial class ToolshedManager } // augh, slow path! - foreach (var (param, genParser) in _constrainedParsers) + foreach (var (_, genParser) in _constrainedParsers) { // no, IsAssignableTo isn't useful here. I tried. tfw. try @@ -92,7 +191,9 @@ public sealed partial class ToolshedManager var concreteParser = genParser.MakeGenericType(t); var builtParser = (ITypeParser) _typeFactory.CreateInstanceUnchecked(concreteParser, true); - builtParser.PostInject(); + if (builtParser is IPostInjectInit inj) + inj.PostInject(); + return builtParser; } catch (SecurityException) @@ -128,51 +229,62 @@ public sealed partial class ToolshedManager /// A console error, if any, that can be reported to explain the parsing failure. /// The type to parse from the input. /// Success. - public bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out T? parsed, out IConError? error) + public bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out T? parsed) { - var res = TryParse(parserContext, typeof(T), out var p, out error); + var res = TryParse(parserContext, typeof(T), out var p); if (p is not null) parsed = (T?) p; else - parsed = default(T); + parsed = default; return res; } /// /// iunno man it does autocomplete what more do u want /// - public ValueTask<(CompletionResult?, IConError?)> TryAutocomplete(ParserContext parserContext, Type t, string? argName) + public CompletionResult? TryAutocomplete(ParserContext ctx, Type t, string? argName) { - var impl = GetParserForType(t); - - if (impl is null) - { - return ValueTask.FromResult<(CompletionResult?, IConError?)>((null, new UnparseableValueError(t))); - } - - return impl.TryAutocomplete(parserContext, argName); + DebugTools.AssertNull(ctx.Error); + DebugTools.AssertNull(ctx.Completions); + DebugTools.AssertEqual(ctx.GenerateCompletions, true); + return GetParserForType(t)?.TryAutocomplete(ctx, argName); } /// - /// Attempts to parse the given type. + /// Attempts to parse the given type directly. Unlike this will not attempt + /// to resolve variable or command blocks. /// /// The input to parse from. /// The type to parse from the input. /// The parsed value, if any. - /// A console error, if any, that can be reported to explain the parsing failure. /// Success. - public bool TryParse(ParserContext parserContext, Type t, [NotNullWhen(true)] out object? parsed, out IConError? error) + public bool TryParse(ParserContext parserContext, Type t, [NotNullWhen(true)] out object? parsed) { - var impl = GetParserForType(t); + parsed = null; - if (impl is null) + if (GetParserForType(t) is not {} impl) { - parsed = null; - error = new UnparseableValueError(t); + if (!parserContext.GenerateCompletions) + parserContext.Error = new UnparseableValueError(t); return false; } - return impl.TryParse(parserContext, out parsed, out error); + if (!impl.TryParse(parserContext, out parsed)) + return false; + + DebugTools.Assert(parsed.GetType().IsAssignableTo(t)); + return true; + } + + /// + /// Variant of that will first attempt to parse the argument as a + /// or , before falling back to the default parser. Note that this generally does not directly + /// return the requested type. + /// + public bool TryParseArgument(ParserContext parserContext, Type t, [NotNullWhen(true)] out object? parsed) + { + parsed = null; + return GetArgumentParser(t) is { } parser && parser.TryParse(parserContext, out parsed); } } @@ -195,10 +307,8 @@ public record UnparseableValueError(Type T) : IConError msg.AddMarkupOrThrow("[bold][color=red]THIS IS A BUG.[/color][/bold]"); return msg; } - else - { - return FormattedMessage.FromUnformatted($"The type {T.PrettyName()} cannot be parsed, as it cannot be constructed."); - } + + return FormattedMessage.FromUnformatted($"The type {T.PrettyName()} cannot be parsed, as it cannot be constructed."); } public string? Expression { get; set; } diff --git a/Robust.Shared/Toolshed/ToolshedManager.Permissions.cs b/Robust.Shared/Toolshed/ToolshedManager.Permissions.cs index 6642cb5fe..19c4f72de 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.Permissions.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.Permissions.cs @@ -1,4 +1,6 @@ -#if !CLIENT_SCRIPTING +using Robust.Shared.Player; +using Robust.Shared.Toolshed.Errors; +#if !CLIENT_SCRIPTING using System; #endif @@ -16,6 +18,19 @@ public sealed partial class ToolshedManager /// public IPermissionController? ActivePermissionController { get; set; } + /// + /// Check whether a command can be invoked by the given session/user. + /// A null session implies that the command is being run by the server. + /// + public bool CheckInvokable(CommandSpec command, ICommonSession? session, out IConError? error) + { + if (ActivePermissionController is { } controller) + return controller.CheckInvokable(command, session, out error); + + error = null; + return true; + } + public ToolshedEnvironment DefaultEnvironment { get diff --git a/Robust.Shared/Toolshed/ToolshedManager.Types.cs b/Robust.Shared/Toolshed/ToolshedManager.Types.cs index 79ae9174e..500a22f38 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.Types.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.Types.cs @@ -9,6 +9,7 @@ namespace Robust.Shared.Toolshed; public sealed partial class ToolshedManager { + // If this gets updated, ensure that GetTransformer() is also updated internal bool IsTransformableTo(Type left, Type right) { if (left.IsAssignableToGeneric(right, this)) @@ -21,25 +22,16 @@ public sealed partial class ToolshedManager return true; } - if (right == typeof(object)) - return true; // May need boxed. - - if (right.IsGenericType && right.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - if (right.GenericTypeArguments[0] == left) - return true; - + if (!right.IsGenericType(typeof(IEnumerable<>))) return false; - } - return false; + return right.GenericTypeArguments[0] == left; } + // Autobots, roll out! + // If this gets updated, ensure that IsTransformableTo() is also updated internal Expression GetTransformer(Type from, Type to, Expression input) { - if (!IsTransformableTo(from, to)) - throw new InvalidCastException(); - if (from.IsAssignableTo(to)) return Expression.Convert(input, to); @@ -54,20 +46,23 @@ public sealed partial class ToolshedManager ); } - if (to.IsGenericType && to.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + if (to.IsGenericType(typeof(IEnumerable<>))) { var toInner = to.GenericTypeArguments[0]; - var tys = new [] {toInner}; + var tys = new[] {toInner}; return Expression.Convert( Expression.New( - typeof(UnitEnumerable<>).MakeGenericType(tys).GetConstructor(tys)!, - Expression.Convert(input, toInner) - ), - to - ); + typeof(UnitEnumerable<>).MakeGenericType(tys).GetConstructor(tys)!, + Expression.Convert(input, toInner) + ), + to + ); } - return Expression.Convert(input, to); + if (from.IsAssignableToGeneric(to, this)) + return Expression.Convert(input, to); + + throw new InvalidCastException(); } } diff --git a/Robust.Shared/Toolshed/ToolshedManager.cs b/Robust.Shared/Toolshed/ToolshedManager.cs index 97d53883a..0ec85f601 100644 --- a/Robust.Shared/Toolshed/ToolshedManager.cs +++ b/Robust.Shared/Toolshed/ToolshedManager.cs @@ -1,12 +1,15 @@ using System.Collections.Generic; +using System.Diagnostics; using Robust.Shared.Console; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.IoC; using Robust.Shared.Log; +using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Reflection; +using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Invocation; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; @@ -130,7 +133,6 @@ public sealed partial class ToolshedManager return InvokeCommand(ctx, command, input, out result); } - /// /// Invokes a command with the given context. /// @@ -148,21 +150,36 @@ public sealed partial class ToolshedManager public bool InvokeCommand(IInvocationContext ctx, string command, object? input, out object? result) { ctx.ClearErrors(); + result = null; - var parser = new ParserContext(command, this, ctx.Environment); - if (!CommandRun.TryParse(false, parser, input?.GetType(), null, false, out var expr, out _, out var err) || parser.Index < parser.MaxIndex) + var parser = new ParserContext(command, this, ctx); + if (!CommandRun.TryParse(parser, input?.GetType(), null, out var expr)) { - - if (err is not null) - ctx.ReportError(err); - - result = null; + ctx.ReportError(parser.Error ?? new FailedToParseError()); return false; } result = expr.Invoke(input, ctx); return true; } + + public CompletionResult? GetCompletions(ConsoleShell shell, string command) + { + var idx = shell.Player?.UserId ?? new NetUserId(); + if (!_contexts.TryGetValue(idx, out var ourCtx)) + ourCtx = _contexts[idx] = new OldShellInvocationContext(shell); + + return GetCompletions(ourCtx, command); + } + + public CompletionResult? GetCompletions(IInvocationContext ctx, string command) + { + ctx.ClearErrors(); + var parser = new ParserContext(command, this, ctx); + parser.GenerateCompletions = true; + CommandRun.TryParse(parser, null, null, out _); + return parser.Completions; + } } /// @@ -183,25 +200,31 @@ public readonly record struct CommandSpec(ToolshedCommand Cmd, string? SubComman /// public CompletionOption AsCompletion() { - return new CompletionOption( - $"{Cmd.Name}{(SubCommand is not null ? ":" + SubCommand : "")}", - Cmd.Description(SubCommand) - ); + return new CompletionOption(FullName(), Cmd.Description(SubCommand)); } /// /// Returns the full name of the command. /// - public string FullName() => $"{Cmd.Name}{(SubCommand is not null ? ":" + SubCommand : "")}"; + public string FullName() => SubCommand == null ? Cmd.Name : $"{Cmd.Name}:{SubCommand}"; /// /// Returns the localization string for the description of this command. /// - public string DescLocStr() => Cmd.UnlocalizedDescription(SubCommand); + public string DescLocStr() => Cmd.DescriptionLocKey(SubCommand); /// - public override string ToString() - { - return Cmd.GetHelp(SubCommand); - } + public override string ToString() => FullName(); +} + +public record struct FailedToParseError() : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Failed to parse toolshed command"); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockType.cs b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs new file mode 100644 index 000000000..cf4a4d238 --- /dev/null +++ b/Robust.Shared/Toolshed/TypeParsers/BlockType.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Console; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Toolshed.Syntax; + +namespace Robust.Shared.Toolshed.TypeParsers; + +/// +/// This custom type parser is used for parsing the type returned by a command argument. +/// +public sealed class BlockOutputParser : CustomTypeParser +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) + { + result = null; + var save = ctx.Save(); + var start = ctx.Index; + if (!Block.TryParseBlock(ctx, null, null, out var block)) + { + ctx.Error?.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + ctx.Restore(save); + + if (block.ReturnType == null) + return false; + + result = block.ReturnType; + return true; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + Block.TryParseBlock(ctx, null, null, out _); + return ctx.Completions; + } +} + +// TODO TOOLSHED Improve Block-type parsers +// See the comment in the remark.. Ideally the type parser should be able to know this. +// But currently type parsers are only used once per command, not once per method/implementation. +/// +/// This custom type parser is used for parsing the type returned by a , where the block's input +/// type is inferred from the type being piped into the command that is currently being parsed. +/// +/// +/// If the piped type is an , it is assumed that the blocks input type is the enumerable +/// generic argument. I.e., we assume that the command has an implementation where the parameter with the +/// is also an +/// > +public sealed class MapBlockOutputParser : CustomTypeParser +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) + { + result = null; + + var pipeType = ctx.Bundle.PipedType; + if (pipeType != null && pipeType.IsGenericType(typeof(IEnumerable<>))) + pipeType = pipeType.GetGenericArguments()[0]; + + var save = ctx.Save(); + var start = ctx.Index; + if (!Block.TryParseBlock(ctx, pipeType, null, out var block)) + { + ctx.Error?.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + + ctx.Restore(save); + + if (block.ReturnType == null) + return false; + + result = block.ReturnType; + return true; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + var pipeType = ctx.Bundle.PipedType; + if (pipeType != null && pipeType.IsGenericType(typeof(IEnumerable<>))) + pipeType = pipeType.GetGenericArguments()[0]; + + Block.TryParseBlock(ctx, pipeType, null, out _); + return ctx.Completions; + } +} diff --git a/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs index d14c2d18d..0945cb5ff 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BlockTypeParser.cs @@ -1,80 +1,47 @@ using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; -using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.TypeParsers; internal sealed class BlockTypeParser : TypeParser { - public BlockTypeParser() + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? result) { + return Block.TryParse(ctx, out result); } - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) { - var r = Block.TryParse(false, parserContext, null, out var block, out _, out error); - result = block; - return r; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - Block.TryParse(true, parserContext, null, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; + Block.TryParse(ctx, out _); + return ctx.Completions; } } - internal sealed class BlockTypeParser : TypeParser> { - public BlockTypeParser() + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? result) { + return Block.TryParse(ctx, out result); } - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) { - var r = Block.TryParse(false, parserContext, null, out var block, out _, out error); - result = block; - return r; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - Block.TryParse(true, parserContext, null, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; + Block.TryParse(ctx, out _); + return ctx.Completions; } } internal sealed class BlockTypeParser : TypeParser> { - public BlockTypeParser() + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block? result) { + return Block.TryParse(ctx, out result); } - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) { - var r = Block.TryParse(false, parserContext, null, out var block, out _, out error); - result = block; - return r; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - Block.TryParse(true, parserContext, null, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; + Block.TryParse(ctx, out _); + return ctx.Completions; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs index 17c2a87d4..3621b0425 100644 --- a/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/BoolTypeParser.cs @@ -1,6 +1,4 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -11,48 +9,43 @@ namespace Robust.Shared.Toolshed.TypeParsers; public sealed class BoolTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out bool result) { - var word = parserContext.GetWord(ParserContext.IsToken)?.ToLowerInvariant(); + var word = ctx.GetWord(ParserContext.IsToken)?.ToLowerInvariant(); if (word is null) { - if (parserContext.PeekChar() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); - result = null; - return false; - } - else - { - error = new InvalidBool(parserContext.GetWord()!); - result = null; + ctx.Error = new OutOfInputError(); + result = default; return false; } + + ctx.Error = new InvalidBool(ctx.GetWord()!); + result = default; + return false; } if (word == "true" || word == "t" || word == "1") { result = true; - error = null; return true; } - else if (word == "false" || word == "f" || word == "0") + + if (word == "false" || word == "f" || word == "0") { result = false; - error = null; return true; } - else - { - error = new InvalidBool(word); - result = null; - return false; - } + + ctx.Error = new InvalidBool(word); + result = default; + return false; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromOptions(new[] { "true", "false" }), null)); + return CompletionResult.FromOptions(new[] {"true", "false"}); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs new file mode 100644 index 000000000..69593f254 --- /dev/null +++ b/Robust.Shared/Toolshed/TypeParsers/CommandRunTypeParser.cs @@ -0,0 +1,47 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Console; +using Robust.Shared.Toolshed.Syntax; + +namespace Robust.Shared.Toolshed.TypeParsers; + +internal sealed class CommandRunTypeParser : TypeParser +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result) + { + return CommandRun.TryParse(ctx, null, null, out result); + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + CommandRun.TryParse(ctx, null, null, out _); + return ctx.Completions; + } +} + +internal sealed class ExpressionTypeParser : TypeParser> +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result) + { + return CommandRun.TryParse(ctx, null, out result); + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + CommandRun.TryParse(ctx, null, out _); + return ctx.Completions; + } +} + +internal sealed class ExpressionTypeParser : TypeParser> +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result) + { + return CommandRun.TryParse(ctx, out result); + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + CommandRun.TryParse(ctx, out _); + return ctx.Completions; + } +} diff --git a/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs index c5fb137ca..f994b59da 100644 --- a/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/CommandSpecTypeParser.cs @@ -1,73 +1,67 @@ -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; +using System.Linq; using Robust.Shared.Console; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.TypeParsers; public sealed class CommandSpecTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out CommandSpec result) { - var cmd = parserContext.GetWord(ParserContext.IsCommandToken); - var start = parserContext.Index; + var cmd = ctx.GetWord(ParserContext.IsCommandToken); + var start = ctx.Index; string? subCommand = null; if (cmd is null) { - if (parserContext.PeekRune() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - result = null; + ctx.Error = new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index)); + result = default; return false; } - else - { - error = new NotValidCommandError(typeof(object)); - error.Contextualize(parserContext.Input, (start, parserContext.Index+1)); - result = null; - return false; - } + ctx.Error = new NotValidCommandError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); + result = default; + return false; } - if (!parserContext.Environment.TryGetCommand(cmd, out var cmdImpl)) + if (!ctx.Environment.TryGetCommand(cmd, out var cmdImpl)) { - error = new UnknownCommandError(cmd); - error.Contextualize(parserContext.Input, (start, parserContext.Index)); - result = null; + ctx.Error = new UnknownCommandError(cmd); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); + result = default; return false; } if (cmdImpl.HasSubCommands) { - error = null; - - if (parserContext.GetChar() is not ':') + if (!ctx.EatMatch(':')) { - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - result = null; + ctx.Error = ctx.OutOfInput ? new OutOfInputError() : new ExpectedSubCommand(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index + 1)); + result = default; return false; } - var subCmdStart = parserContext.Index; + var subCmdStart = ctx.Index; - if (parserContext.GetWord(ParserContext.IsToken) is not { } subcmd) + if (ctx.GetWord(ParserContext.IsToken) is not { } subcmd) { - error = new OutOfInputError(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index)); - result = null; + ctx.Error = new ExpectedSubCommand(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index)); + result = default; return false; } if (!cmdImpl.Subcommands.Contains(subcmd)) { - error = new UnknownSubcommandError(cmd, subcmd, cmdImpl); - error.Contextualize(parserContext.Input, (subCmdStart, parserContext.Index)); - result = null; + ctx.Error = new UnknownSubcommandError(subcmd, cmdImpl); + ctx.Error.Contextualize(ctx.Input, (subCmdStart, ctx.Index)); + result = default; return false; } @@ -75,13 +69,21 @@ public sealed class CommandSpecTypeParser : TypeParser } result = new CommandSpec(cmdImpl, subCommand); - error = null; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { var cmds = parserContext.Environment.AllCommands(); - return ValueTask.FromResult<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""), null)); + return CompletionResult.FromHintOptions(cmds.Select(x => x.AsCompletion()), ""); + } +} + + +public sealed class ExpectedSubCommand : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Expected subcommand"); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs index fd8cad942..12ff01a90 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ComponentTypeParser.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; @@ -14,49 +13,40 @@ using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.TypeParsers; -internal sealed class ComponentTypeParser : TypeParser +public sealed class ComponentTypeParser : CustomTypeParser { [Dependency] private readonly IComponentFactory _factory = default!; - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) { - var start = parserContext.Index; - var word = parserContext.GetWord(ParserContext.IsToken); - error = null; + result = null; + var start = ctx.Index; + var word = ctx.GetWord(ParserContext.IsToken); if (word is null) { - error = new OutOfInputError(); - result = null; + ctx.Error = new OutOfInputError(); return false; } if (!_factory.TryGetRegistration(word.ToLower(), out var reg, true)) { - result = null; - error = new UnknownComponentError(word); - error.Contextualize(parserContext.Input, (start, parserContext.Index)); + ctx.Error = new UnknownComponentError(word); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); return false; } - result = new ComponentType(reg.Type); + result = reg.Type; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>( - (CompletionResult.FromOptions(_factory.AllRegisteredTypes.Select(_factory.GetComponentName)), null) - ); + return CompletionResult.FromOptions(_factory.AllRegisteredTypes.Select(_factory.GetComponentName)); } } -public readonly record struct ComponentType(Type Ty) : IAsType -{ - public Type AsType() => Ty; -}; - public record struct UnknownComponentError(string Component) : IConError { public FormattedMessage DescribeInner() diff --git a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs index e452d06a8..bd5872cb9 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs @@ -1,10 +1,8 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; +using System.Collections.Generic; using Robust.Shared.Console; using Robust.Shared.GameObjects; using Robust.Shared.IoC; -using Robust.Shared.Maths; +using Robust.Shared.Toolshed.Commands.Entities; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Utility; @@ -13,58 +11,242 @@ namespace Robust.Shared.Toolshed.TypeParsers; internal sealed class EntityTypeParser : TypeParser { - [Dependency] private readonly IEntityManager _entityManager = default!; + [Dependency] private readonly IEntityManager _entMan = default!; - public override bool TryParse(ParserContext parser, [NotNullWhen(true)] out object? result, out IConError? error) + public static bool TryParseEntity(IEntityManager entMan, ParserContext ctx, out EntityUid result) { - var start = parser.Index; - var word = parser.GetWord(ParserContext.IsToken); - error = null; + string? word; + var start = ctx.Index; - if (!NetEntity.TryParse(word, out var ent)) + // e prefix implies we should parse the number as an EntityUid directly, not as a NetEntity + // Note that this breaks auto completion results + if (ctx.EatMatch('e')) { - result = null; + word = ctx.GetWord(ParserContext.IsToken); + if (EntityUid.TryParse(word, out result)) + return true; - if (word is not null) - error = new InvalidEntity(word); - else - error = new OutOfInputError(); - - error.Contextualize(parser.Input, (start, parser.Index)); + ctx.Error = word is not null ? new InvalidEntity($"e{word}") : new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); return false; } - result = _entityManager.GetEntity(ent); + // Optional 'n' prefix for differentiating whether an integer represents a NetEntity or EntityUid + ctx.EatMatch('n'); + word = ctx.GetWord(ParserContext.IsToken); + + if (NetEntity.TryParse(word, out var ent)) + { + result = entMan.GetEntity(ent); + return true; + } + + result = default; + + ctx.Error = word is not null ? new InvalidEntity(word) : new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + + public override bool TryParse(ParserContext parser, out EntityUid result) + { + return TryParseEntity(_entMan, parser, out result); + } + + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (NetEntity)"); +} + +internal sealed class NetEntityTypeParser : TypeParser +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + public override bool TryParse(ParserContext ctx, out NetEntity result) + { + // This doesn't just directly call the EntityUid parser, as the client might be trying to parse a NetEntity to + // send to the server, even though it doesn't actually know about / has not encountered. + + string? word; + var start = ctx.Index; + + // e prefix implies we should parse the number as an EntityUid directly, not as a NetEntity + // Note that this breaks auto completion results + if (ctx.EatMatch('e')) + { + word = ctx.GetWord(ParserContext.IsToken); + if (EntityUid.TryParse(word, out var euid)) + { + result = _entMan.GetNetEntity(euid); + return true; + } + + result = default; + ctx.Error = word is not null ? new InvalidEntity($"e{word}") : new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + + // Optional 'n' prefix for differentiating whether an integer represents a NetEntity or EntityUid + ctx.EatMatch('n'); + word = ctx.GetWord(ParserContext.IsToken); + + if (NetEntity.TryParse(word, out result)) + return true; + + result = default; + + ctx.Error = word is not null ? new InvalidEntity(word) : new OutOfInputError(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); + return false; + } + + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + => CompletionResult.FromHint(argName == null ? "" : $"<{argName}> (NetEntity)"); +} + +internal sealed class EntityTypeParser : TypeParser> + where T : IComponent +{ + [Dependency] private readonly IEntityManager _entMan = default!; + + public override bool TryParse(ParserContext parser, out Entity result) + { + result = default; + if (!EntityTypeParser.TryParseEntity(_entMan, parser, out var uid)) + return false; + + if (!_entMan.TryGetComponent(uid, out T? comp)) + return false; + + result = new(uid, comp); return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) { - return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromHint(""), null)); + // Avoid commands with loose permissions accidentally leaking information about entities. + // I.e., if some command had an Entity argument, we don't want auto-completions for + // that to allow people to get a list of all players/minds when they shouldn't know that. + if (!ctx.CheckInvokable()) + return null; + + var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + + // Avoid dumping too many entities + if (_entMan.Count() > 128) + return CompletionResult.FromHint(hint); + + var query = _entMan.AllEntityQueryEnumerator(); + var list = new List(); + while (query.MoveNext(out _, out var metadata)) + { + list.Add(new CompletionOption(metadata.NetEntity.ToString(), metadata.EntityName)); + } + + return CompletionResult.FromHintOptions(list, hint); } } -public record InvalidEntity(string Value) : IConError +internal sealed class EntityTypeParser : TypeParser> + where T1 : IComponent + where T2 : IComponent { - public FormattedMessage DescribeInner() + [Dependency] private readonly IEntityManager _entMan = default!; + + public override bool TryParse(ParserContext parser, out Entity result) { - return FormattedMessage.FromUnformatted($"Couldn't parse {Value} as an Entity."); + result = default; + if (!EntityTypeParser.TryParseEntity(_entMan, parser, out var uid)) + return false; + + if (!_entMan.TryGetComponent(uid, out T1? comp1)) + return false; + + if (!_entMan.TryGetComponent(uid, out T2? comp2)) + return false; + + result = new(uid, comp1, comp2); + return true; } - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + if (!ctx.CheckInvokable()) + return null; + + var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + if (_entMan.Count() > 128) + return CompletionResult.FromHint(hint); + + var query = _entMan.AllEntityQueryEnumerator(); + var list = new List(); + while (query.MoveNext(out _, out _, out var metadata)) + { + list.Add(new CompletionOption(metadata.NetEntity.ToString(), metadata.EntityName)); + } + + return CompletionResult.FromHintOptions(list, hint); + } } -public record DeadEntity(EntityUid Entity) : IConError +internal sealed class EntityTypeParser : TypeParser> + where T1 : IComponent + where T2 : IComponent + where T3 : IComponent { - public FormattedMessage DescribeInner() + [Dependency] private readonly IEntityManager _entMan = default!; + + public override bool TryParse(ParserContext parser, out Entity result) { - return FormattedMessage.FromUnformatted($"The entity {Entity} does not exist."); + result = default; + if (!EntityTypeParser.TryParseEntity(_entMan, parser, out var uid)) + return false; + + if (!_entMan.TryGetComponent(uid, out T1? comp1)) + return false; + + if (!_entMan.TryGetComponent(uid, out T2? comp2)) + return false; + + if (!_entMan.TryGetComponent(uid, out T3? comp3)) + return false; + + result = new(uid, comp1, comp2, comp3); + return true; } - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + if (!ctx.CheckInvokable()) + return null; + + var hint = argName == null ? "" : $"<{argName}> (NetEntity)"; + if (_entMan.Count() > 128) + return CompletionResult.FromHint(hint); + + var query = _entMan.AllEntityQueryEnumerator(); + var list = new List(); + while (query.MoveNext(out _, out _, out _, out var metadata)) + { + list.Add(new CompletionOption(metadata.NetEntity.ToString(), metadata.EntityName)); + } + + return CompletionResult.FromHintOptions(list, hint); + } +} + +public sealed class InvalidEntity(string value) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Couldn't parse {value} as an Entity."); + } +} + +public sealed class DeadEntity(EntityUid entity) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"The entity {entity} does not exist."); + } } diff --git a/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs index 85e27dea5..ceaa52f52 100644 --- a/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/EnumTypeParser.cs @@ -1,7 +1,6 @@ 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; @@ -13,42 +12,37 @@ namespace Robust.Shared.Toolshed.TypeParsers; public sealed class EnumTypeParser : TypeParser where T : unmanaged, Enum { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, - out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T result) { - var word = parserContext.GetWord(ParserContext.IsToken); + var word = ctx.GetWord(ParserContext.IsToken); if (word is null) { - if (parserContext.PeekChar() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); - result = null; + ctx.Error = new OutOfInputError(); + result = default; return false; } - else - { - error = new InvalidEnum(parserContext.GetWord()!); - result = null; - return false; - } + ctx.Error = new InvalidEnum(ctx.GetWord()!); + result = default; + return false; } if (!Enum.TryParse(word, ignoreCase: true, out var value)) { - result = null; - error = new InvalidEnum(word); + result = default; + ctx.Error = new InvalidEnum(word); return false; } result = value; - error = null; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromOptions(Enum.GetNames()), null)); + return CompletionResult.FromOptions(Enum.GetNames()); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ExpressionTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ExpressionTypeParser.cs deleted file mode 100644 index 939286dfe..000000000 --- a/Robust.Shared/Toolshed/TypeParsers/ExpressionTypeParser.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Robust.Shared.Console; -using Robust.Shared.Toolshed.Errors; -using Robust.Shared.Toolshed.Syntax; - -namespace Robust.Shared.Toolshed.TypeParsers; - -internal sealed class ExpressionTypeParser : TypeParser -{ - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) - { - var res = CommandRun.TryParse(false, parserContext, null, null, false, out var r, out _, out error); - result = r; - return res; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - CommandRun.TryParse(true, parserContext, null, null, false, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; - } - -} - -internal sealed class ExpressionTypeParser : TypeParser> -{ - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) - { - var res = CommandRun.TryParse(false, false, parserContext, null, false, out var r, out _, out error); - result = r; - return res; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - CommandRun.TryParse(false, true, parserContext, null, false, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; - } -} - -internal sealed class ExpressionTypeParser : TypeParser> -{ - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) - { - var res = CommandRun.TryParse(false, false, parserContext, false, out var r, out _, out error); - result = r; - return res; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) - { - CommandRun.TryParse(false, true, parserContext, false, out _, out var autocomplete, out _); - if (autocomplete is null) - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((null, null)); - - return autocomplete.Value; - } -} diff --git a/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs index 2f9c58969..5bff38a5c 100644 --- a/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/InstanceIdTypeParser.cs @@ -1,24 +1,20 @@ using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; -using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.TypeParsers; public sealed class InstanceIdTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext parserContext, out InstanceId result) { result = new InstanceId(Guid.NewGuid()); - error = null; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult? result, IConError? error)>((null, null)); + return null; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs index f69839a5b..3aa46faf1 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/AngleTypeParser.cs @@ -1,8 +1,5 @@ -using System; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics; using System.Globalization; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -13,56 +10,50 @@ namespace Robust.Shared.Toolshed.TypeParsers.Math; public sealed class AngleTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out Angle result) { - var word = parserContext.GetWord(ParserContext.IsNumeric)?.ToLowerInvariant(); + result = default; + var word = ctx.GetWord(ParserContext.IsNumeric)?.ToLowerInvariant(); if (word is null) { - if (parserContext.PeekChar() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); - result = null; - return false; - } - else - { - error = new InvalidAngle(parserContext.GetWord()!); - result = null; + ctx.Error = new OutOfInputError(); return false; } + + ctx.Error = new InvalidAngle(ctx.GetWord()!); + return false; } if (word.EndsWith("deg")) { if (!float.TryParse(word[..^3], CultureInfo.InvariantCulture, out var f)) { - error = new InvalidAngle(word); - result = null; + ctx.Error = new InvalidAngle(word); return false; } result = Angle.FromDegrees(f); - error = null; return true; } else { if (!float.TryParse(word, CultureInfo.InvariantCulture, out var f)) { - error = new InvalidAngle(word); - result = null; + ctx.Error = new InvalidAngle(word); + result = default; return false; } result = new Angle(f); - error = null; return true; } } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult? result, IConError? error)>((CompletionResult.FromHint("angle (append deg for degrees, otherwise radians)"), null)); + return CompletionResult.FromHint("angle (append deg for degrees, otherwise radians)"); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs index 7aac2c5e0..284fa393d 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/ColorTypeParser.cs @@ -1,11 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; +using System.Diagnostics; using System.Linq; using System.Text; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -16,43 +11,35 @@ namespace Robust.Shared.Toolshed.TypeParsers.Math; public sealed class ColorTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out Color result) { - var word = parserContext.GetWord(x => ParserContext.IsToken(x) || x == new Rune('#'))?.ToLowerInvariant(); + result = default; + var word = ctx.GetWord(x => ParserContext.IsToken(x) || x == new Rune('#'))?.ToLowerInvariant(); if (word is null) { - if (parserContext.PeekChar() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); - result = null; + ctx.Error = new OutOfInputError(); return false; } - else - { - error = new InvalidColor(parserContext.GetWord()!); - result = null; - return false; - } - } - if (!Color.TryParse(word, out var r)) - { - error = new InvalidColor(word); - result = null; + ctx.Error = new InvalidColor(ctx.GetWord()!); + result = default; return false; } - result = r; - error = null; - return true; + if (Color.TryParse(word, out result)) + return true; + + ctx.Error = new InvalidColor(word); + return false; + } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult? result, IConError? error)>(( - CompletionResult.FromHintOptions(Color.GetAllDefaultColors().Select(x => x.Key), "RGB color or color name."), - null - )); + return CompletionResult.FromHintOptions(Color.GetAllDefaultColors().Select(x => x.Key), + "RGB color or color name."); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs index bac588db1..4f1fadc4d 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/NumberBaseTypeParser.cs @@ -2,7 +2,6 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Numerics; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -14,41 +13,35 @@ namespace Robust.Shared.Toolshed.TypeParsers.Math; internal sealed class NumberBaseTypeParser : TypeParser where T: INumberBase { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result) { - var maybeNumber = parserContext.GetWord(ParserContext.IsNumeric); - if (maybeNumber?.Length == 0) + result = default; + var maybeNumber = ctx.GetWord(ParserContext.IsNumeric); + if (string.IsNullOrEmpty(maybeNumber)) { - error = new OutOfInputError(); - result = null; + ctx.Error = new ExpectedNumericError(); return false; } - if (!T.TryParse(maybeNumber, NumberStyles.Number, CultureInfo.InvariantCulture, out var @number)) - { - if (maybeNumber is null) - { - error = new OutOfInputError(); - } - else - { - error = new InvalidNumber(maybeNumber); - } + if (T.TryParse(maybeNumber, NumberStyles.Number, CultureInfo.InvariantCulture, out result)) + return true; - result = null; - return false; - } - result = @number; - error = null; - return true; + ctx.Error = new InvalidNumber(maybeNumber); + return false; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return new ValueTask<(CompletionResult? result, IConError? error)>( - (CompletionResult.FromHint(typeof(T).PrettyName()), null) - ); + return CompletionResult.FromHint(typeof(T).PrettyName()); + } +} + +public sealed class ExpectedNumericError : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Expected a number"); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs index 34199ece3..d35b9c0d9 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Math/SpanLikeTypeParser.cs @@ -1,9 +1,7 @@ using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; -using Robust.Shared.IoC; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; @@ -15,80 +13,75 @@ public abstract class SpanLikeTypeParser : TypeParser where T : notnull where TElem : unmanaged { - [Dependency] private readonly ToolshedManager _toolshed = default!; - public abstract int Elements { get; } public abstract T Create(Span elements); - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result) { - if (!parserContext.EatMatch('[')) + if (!ctx.EatMatch('[')) { - error = new ExpectedOpenBrace(); - result = null; + ctx.Error = new ExpectedOpenBrace(); + result = default; return false; } - parserContext.ConsumeWhitespace(); + ctx.ConsumeWhitespace(); - parserContext.PushTerminator("]"); + ctx.PushBlockTerminator(']'); Span elements = stackalloc TElem[Elements]; for (var i = 0; i < Elements; i++) { - var checkpoint = parserContext.Save(); - if (!_toolshed.TryParse(parserContext, out var value, out error)) + var checkpoint = ctx.Save(); + if (!Toolshed.TryParse(ctx, out var value)) { - parserContext.Restore(checkpoint); + ctx.Restore(checkpoint); - var start = parserContext.Index; - if (parserContext.EatTerminator()) + var start = ctx.Index; + if (ctx.EatBlockTerminator()) { - error = new UnexpectedCloseBrace(); - error.Contextualize(parserContext.Input, new Vector2i(start, parserContext.Index)); + ctx.Error = new UnexpectedCloseBrace(); + ctx.Error.Contextualize(ctx.Input, new Vector2i(start, ctx.Index)); } - result = null; + result = default; return false; } - parserContext.ConsumeWhitespace(); + ctx.ConsumeWhitespace(); - if (i + 1 < Elements && parserContext.EatTerminator()) + if (i + 1 < Elements && ctx.EatBlockTerminator()) { - error = new UnexpectedCloseBrace(); - result = null; + ctx.Error = new UnexpectedCloseBrace(); + result = default; return false; } - if (i + 1 < Elements && !parserContext.TryMatch(",")) + if (i + 1 < Elements && !ctx.EatMatch(',')) { - error = new ExpectedComma(); - result = null; + ctx.Error = new ExpectedComma(); + result = default; return false; } elements[i] = value; - parserContext.ConsumeWhitespace(); + ctx.ConsumeWhitespace(); } - - - if (!parserContext.EatTerminator()) + if (!ctx.EatBlockTerminator()) { - error = new ExpectedCloseBrace(); - result = null; + ctx.Error = new ExpectedCloseBrace(); + result = default; return false; } - error = null; result = Create(elements); return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((CompletionResult.FromHint(typeof(T).PrettyName()), null)); + return CompletionResult.FromHint(typeof(T).PrettyName()); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ProtoIdTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ProtoIdTypeParser.cs deleted file mode 100644 index 99ca63f29..000000000 --- a/Robust.Shared/Toolshed/TypeParsers/ProtoIdTypeParser.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Robust.Shared.Console; -using Robust.Shared.IoC; -using Robust.Shared.Maths; -using Robust.Shared.Prototypes; -using Robust.Shared.Toolshed.Errors; -using Robust.Shared.Toolshed.Syntax; -using Robust.Shared.Utility; - -namespace Robust.Shared.Toolshed.TypeParsers; - -internal sealed class ProtoIdTypeParser : TypeParser> - where T : class, IPrototype -{ - [Dependency] private readonly IPrototypeManager _prototype = default!; - - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) - { - var proto = parserContext.GetWord(ParserContext.IsToken); - - if (proto is null || !_prototype.HasMapping(proto)) - { - _prototype.TryGetKindFrom(out var kind); - DebugTools.AssertNotNull(kind); - - error = new NotAValidProtoId(proto ?? "[null]", kind!); - result = null; - return false; - } - - result = new ProtoId(proto); - error = null; - return true; - } - - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) - { - var options = CompletionHelper.PrototypeIDs(); - - _prototype.TryGetKindFrom(out var kind); - DebugTools.AssertNotNull(kind); - - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((CompletionResult.FromHintOptions(options, $"<{kind} protoId>"), null)); - } -} - -public record NotAValidProtoId(string Proto, string Kind) : IConError -{ - public FormattedMessage DescribeInner() - { - return FormattedMessage.FromMarkupOrThrow($"{Proto} is not a valid {Kind} prototype"); - } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } -} diff --git a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs index 56f28aae5..64f6088c2 100644 --- a/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs @@ -1,11 +1,9 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; +using System.Text; using Robust.Shared.Console; using Robust.Shared.IoC; -using Robust.Shared.Maths; using Robust.Shared.Prototypes; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; @@ -13,31 +11,133 @@ using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.TypeParsers; +public sealed class ProtoIdTypeParser : TypeParser> + where T : class, IPrototype +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + + private CompletionResult? _completions; + + public override bool TryParse(ParserContext ctx, out ProtoId result) + { + result = default; + string? proto; + + // Prototype ids can be specified without quotes, but for backwards compatibility, we also accept strings with + // quotes, as previously it **had** to be a string + if (ctx.PeekRune() == new Rune('"')) + { + if (!Toolshed.TryParse(ctx, out proto)) + return false; + } + else + { + proto = ctx.GetWord(ParserContext.IsToken); + } + + if (proto is null || !_proto.HasIndex(proto)) + { + _proto.TryGetKindFrom(out var kind); + DebugTools.AssertNotNull(kind); + + ctx.Error = new NotAValidPrototype(proto ?? "[null]", kind!); + result = default; + return false; + } + + result = new(proto); + return true; + } + + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + { + if (_completions != null) + return _completions; + + _proto.TryGetKindFrom(out var kind); + var hint = $"<{kind} prototype>"; + + _completions = _proto.Count() < 256 + ? CompletionResult.FromHintOptions( CompletionHelper.PrototypeIDs(proto: _proto), hint) + : CompletionResult.FromHint(hint); + return _completions; + } +} + +public sealed class EntProtoIdTypeParser : TypeParser +{ + public override bool TryParse(ParserContext ctx, out EntProtoId result) + { + result = default; + if (!Toolshed.TryParse(ctx, out ProtoId proto)) + return false; + + result = new(proto.Id); + return true; + } + + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + { + // TODO TOOLSHED Improve ProtoId completions + // Completion options should be able to communicate to a client that it can populate the options by itself. + // I.e., instead of dumping all entity prototypes on the client, tell it how to generate them locally. + return CompletionResult.FromHint($""); + } +} + +public sealed class PrototypeInstanceTypeParser : TypeParser + where T : class, IPrototype +{ + [Dependency] private readonly IPrototypeManager _proto = default!; + + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result) + { + if (!Toolshed.TryParse(ctx, out string? proto)) + proto = ctx.GetWord(ParserContext.IsToken); + + if (proto != null && _proto.TryIndex(proto, out result)) + return true; + + _proto.TryGetKindFrom(out var kind); + DebugTools.AssertNotNull(kind); + + ctx.Error = new NotAValidPrototype(proto ?? "[null]", kind!); + result = null; + return false; + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + return Toolshed.TryAutocomplete(ctx, typeof(ProtoId), argName); + } +} + +[Obsolete] internal sealed class PrototypeTypeParser : TypeParser> where T : class, IPrototype { [Dependency] private readonly IPrototypeManager _prototype = default!; - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out Prototype result) { - var proto = parserContext.GetWord(ParserContext.IsToken); + if (!Toolshed.TryParse(ctx, out string? proto)) + proto = ctx.GetWord(ParserContext.IsToken); if (proto is null || !_prototype.TryIndex(proto, out var resolved)) { _prototype.TryGetKindFrom(out var kind); DebugTools.AssertNotNull(kind); - error = new NotAValidPrototype(proto ?? "[null]", kind!); - result = null; + ctx.Error = new NotAValidPrototype(proto ?? "[null]", kind!); + result = default; return false; } result = new Prototype(resolved); - error = null; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) { IEnumerable options; @@ -50,10 +150,11 @@ internal sealed class PrototypeTypeParser : TypeParser> _prototype.TryGetKindFrom(out var kind); DebugTools.AssertNotNull(kind); - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((CompletionResult.FromHintOptions(options, $"<{kind} prototype>"), null)); + return CompletionResult.FromHintOptions(options, $"<{kind} prototype>"); } } +[Obsolete("Use ProtoId or EntProtoId, or the prototype directly")] public readonly record struct Prototype(T Value) : IAsType where T : class, IPrototype { @@ -65,14 +166,10 @@ public readonly record struct Prototype(T Value) : IAsType } } -public record NotAValidPrototype(string Proto, string Kind) : IConError +public sealed class NotAValidPrototype(string proto, string kind) : ConError { - public FormattedMessage DescribeInner() + public override FormattedMessage DescribeInner() { - return FormattedMessage.FromUnformatted($"{Proto} is not a valid {Kind} prototype"); + return FormattedMessage.FromUnformatted($"{proto} is not a valid {kind} prototype"); } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs index ac38a8345..73875894a 100644 --- a/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/QuantityTypeParser.cs @@ -1,6 +1,4 @@ using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -11,26 +9,21 @@ namespace Robust.Shared.Toolshed.TypeParsers; internal sealed class QuantityParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, out Quantity result) { - var word = parserContext.GetWord(ParserContext.IsNumeric); - error = null; + result = default; + var word = ctx.GetWord(ParserContext.IsNumeric); if (word?.TrimEnd('%') is not { } maybeParseable || !float.TryParse(maybeParseable, out var v)) { - if (word is not null) - error = new InvalidQuantity(word); - else - error = new OutOfInputError(); + ctx.Error = word is not null ? new InvalidQuantity(word) : new OutOfInputError(); - result = null; return false; } if (v < 0.0) { - error = new InvalidQuantity(word); - result = null; + ctx.Error = new InvalidQuantity(word); return false; } @@ -38,8 +31,7 @@ internal sealed class QuantityParser : TypeParser { if (v > 100.0) { - error = new InvalidQuantity(word); - result = null; + ctx.Error = new InvalidQuantity(word); return false; } @@ -51,10 +43,10 @@ internal sealed class QuantityParser : TypeParser return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((CompletionResult.FromHint($"{argName ?? "quantity"}"), null)); + return CompletionResult.FromHint($"{argName ?? "quantity"}"); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs index 25988fb56..0144e4bdc 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ResPathTypeParser.cs @@ -1,31 +1,24 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Robust.Shared.Console; -using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Console; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.TypeParsers; -internal sealed class ResPathTypeParser : StringTypeParser +internal sealed class ResPathTypeParser : TypeParser { - public override Type Parses => typeof(ResPath); - - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext parserContext, out ResPath result) { - var baseResult = base.TryParse(parserContext, out result, out error); - - if (!baseResult) + result = default; + if (!Toolshed.TryParse(parserContext, out string? str)) return false; - result = new ResPath((string) result!); + result = new ResPath(str); return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return base.TryAutocomplete(parserContext, argName); + // TODO TOOLSHED ResPath Completion + return CompletionResult.FromHint($"\"<{argName ?? nameof(ResPath)}>\""); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs index 684e9fc15..39c6ded7e 100644 --- a/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs @@ -1,6 +1,5 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.IoC; using Robust.Shared.Localization; @@ -19,16 +18,15 @@ internal sealed class SessionTypeParser : TypeParser { [Dependency] private ISharedPlayerManager _player = default!; - public override bool TryParse(ParserContext parser, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ICommonSession? result) { - var start = parser.Index; - var word = parser.GetWord(); - error = null; + var start = ctx.Index; + var word = ctx.GetWord(); result = null; if (word == null) { - error = new OutOfInputError(); + ctx.Error = new OutOfInputError(); return false; } @@ -38,16 +36,15 @@ internal sealed class SessionTypeParser : TypeParser return true; } - error = new InvalidUsername(Loc, word); - error.Contextualize(parser.Input, (start, parser.Index)); + ctx.Error = new InvalidUsername(Loc, word); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index)); return false; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { var opts = CompletionHelper.SessionNames(true, _player); - return new ValueTask<(CompletionResult?, IConError?)>((CompletionResult.FromHintOptions(opts, ""), null)); + return CompletionResult.FromHintOptions(opts, ""); } public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError diff --git a/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs index 7c901803e..dedfa2132 100644 --- a/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/StringTypeParser.cs @@ -1,7 +1,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Text; -using System.Threading.Tasks; using Robust.Shared.Console; using Robust.Shared.Maths; using Robust.Shared.Toolshed.Errors; @@ -10,82 +9,74 @@ using Robust.Shared.Utility; namespace Robust.Shared.Toolshed.TypeParsers; -[Virtual] -internal class StringTypeParser : TypeParser +internal sealed class StringTypeParser : TypeParser { - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + // Completion option for hinting that all strings must start with an quote + private static readonly CompletionOption[] Option = [new("\"", Flags: CompletionOptionFlags.PartialCompletion)]; + + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out string? result) { - error = null; - parserContext.ConsumeWhitespace(); - if (parserContext.PeekRune() != new Rune('"')) + ctx.ConsumeWhitespace(); + if (!ctx.EatMatch('"')) { - if (parserContext.PeekRune() is null) + if (ctx.PeekRune() is null) { - error = new OutOfInputError(); + ctx.Error = new OutOfInputError(); result = null; return false; } - error = new StringMustStartWithQuote(); - error.Contextualize(parserContext.Input, (parserContext.Index, parserContext.Index + 1)); + ctx.Error = new StringMustStartWithQuote(); + ctx.Error.Contextualize(ctx.Input, (ctx.Index, ctx.Index + 1)); result = null; return false; } - parserContext.GetRune(); - var output = new StringBuilder(); - - while (true) + while (ctx.GetRune() is {} r) { - while (parserContext.PeekChar() is not '"' and not '\\' and not null) + if (r == new Rune('"')) { - output.Append(parserContext.GetRune()); + result = output.ToString(); + return true; } - if (parserContext.PeekChar() is '"' or null) + if (r != new Rune('\\')) { - if (parserContext.PeekRune() is null) - { - error = new OutOfInputError(); - result = null; - return false; - } - - parserContext.GetRune(); - break; + output.Append(r); + continue; } - parserContext.GetRune(); // okay it's \ + var escaped = ctx.GetRune(); + if (escaped is null) + continue; - switch (parserContext.GetChar()) + if (r == new Rune('"') || r == new Rune('n') || r == new Rune('\\')) { - case '"': - output.Append('"'); - continue; - case 'n': - output.Append('\n'); - continue; - case '\\': - output.Append('\\'); - continue; - default: - result = null; - // todo: error - return false; + output.Append(escaped); + continue; } + + ctx.Error = new UnknownEscapeSequence(escaped.Value); + result = null; + return false; } - parserContext.ConsumeWhitespace(); - - result = output.ToString(); - return true; + // We ran out of input before encountering the terminating quote. + // Either someone is trying to execute an incomplete command, or more likely, they forgot the terminating quote. + if (!ctx.GenerateCompletions) + ctx.Error = new StringMustEndWithQuote(); + result = null; + return false; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) { - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((CompletionResult.FromHint($"\"<{argName ?? "string"}>\""), null)); + var hint = argName != null ? $"<{argName}> (string)" : ""; + parserContext.ConsumeWhitespace(); + return parserContext.PeekRune() == new Rune('"') + ? CompletionResult.FromHint(hint) + : CompletionResult.FromHintOptions(Option, hint); } } @@ -93,10 +84,22 @@ public record struct StringMustStartWithQuote : IConError { public FormattedMessage DescribeInner() { - return FormattedMessage.FromUnformatted("A string must start with a quote."); + return FormattedMessage.FromUnformatted("A string must start with a quote (\")."); } public string? Expression { get; set; } public Vector2i? IssueSpan { get; set; } public StackTrace? Trace { get; set; } } + +public sealed class StringMustEndWithQuote : ConError +{ + public override FormattedMessage DescribeInner() + => FormattedMessage.FromUnformatted($"String must end with a quote (\")."); +} + +public sealed class UnknownEscapeSequence(Rune c) : ConError +{ + public override FormattedMessage DescribeInner() + => FormattedMessage.FromUnformatted($"Unknown escape sequence: \\{c}"); +} diff --git a/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs index cb5923b4c..3d52f0b18 100644 --- a/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/Tuples/BaseTupleTypeParser.cs @@ -3,9 +3,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Text; -using System.Threading.Tasks; using Robust.Shared.Console; -using Robust.Shared.IoC; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; @@ -14,20 +12,18 @@ namespace Robust.Shared.Toolshed.TypeParsers.Tuples; public abstract class BaseTupleTypeParser : TypeParser where TParses: ITuple { - [IoC.Dependency] private readonly ToolshedManager _toolshed = default!; - public abstract IEnumerable Fields { get; } public abstract TParses Create(IReadOnlyList values); - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out TParses? result) { var values = new List(); foreach (var field in Fields) { - if (!_toolshed.TryParse(parserContext, field, out var parsed, out error)) + if (!Toolshed.TryParse(parserContext, field, out var parsed)) { - result = null; + result = default; return false; } @@ -35,23 +31,22 @@ public abstract class BaseTupleTypeParser : TypeParser } result = Create(values); - error = null; return true; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - IConError? error = null; foreach (var field in Fields) { var checkpoint = parserContext.Save(); - if (!_toolshed.TryParse(parserContext, field, out _, out error) || !Rune.IsWhiteSpace(parserContext.PeekRune() ?? new Rune('.'))) - { - parserContext.Restore(checkpoint); - return _toolshed.TryAutocomplete(parserContext, field, argName); - } + if (Toolshed.TryParse(parserContext, field, out _) && + Rune.IsWhiteSpace(parserContext.PeekRune() ?? new Rune('.'))) + continue; + + parserContext.Restore(checkpoint); + return Toolshed.TryAutocomplete(parserContext, field, argName); } - return new ValueTask<(CompletionResult? result, IConError? error)>((null, null)); + return null; } } diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs index 4d9078709..79e6da76d 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeParser.cs @@ -1,44 +1,89 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; using JetBrains.Annotations; using Robust.Shared.Console; using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Log; -using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; namespace Robust.Shared.Toolshed.TypeParsers; +/// +/// Base interface used by both custom and default type parsers. +/// [UsedImplicitly(ImplicitUseTargetFlags.WithInheritors)] -public interface ITypeParser : IPostInjectInit +internal interface ITypeParser { public Type Parses { get; } + bool TryParse(ParserContext ctx, [NotNullWhen(true)] out object? result); + CompletionResult? TryAutocomplete(ParserContext ctx, string? argName); - public bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error); - - public ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName); + /// + /// If true, then before attempting to use this parser directly, toolshed will instead first try to parse this as a + /// as a variable () or command block (). + /// This has no effect when the parser is being used to parse type-arguments, those don't currently support blocks + /// or variables + /// + public bool EnableValueRef { get; } } -[PublicAPI] -public abstract class TypeParser : ITypeParser - where T: notnull +public abstract class BaseParser : ITypeParser, IPostInjectInit where T : notnull { - [Dependency] private readonly ILogManager _log = default!; + public virtual bool EnableValueRef => true; + + // TODO TOOLSHED Localization + // Ensure that all of the type parser auto-completions actually use localized strings [Dependency] protected readonly ILocalizationManager Loc = default!; + [Dependency] private readonly ILogManager _log = default!; + [Dependency] protected readonly ToolshedManager Toolshed = default!; protected ISawmill Log = default!; - public virtual Type Parses => typeof(T); - - public abstract bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error); - public abstract ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName); - public virtual void PostInject() { Log = _log.GetSawmill(GetType().PrettyName()); } + + public abstract bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result); + public abstract CompletionResult? TryAutocomplete(ParserContext ctx, string? argName); + + public Type Parses => typeof(T); + + bool ITypeParser.TryParse(ParserContext ctx, [NotNullWhen(true)] out object? result) + { + if (!TryParse(ctx, out T? res)) + { + result = null; + return false; + } + + result = res; + return true; + } +} + +/// +/// Inheritors of this class can be used as custom parsers for toolshed commands. Inheritors need to have a +/// parameterless constructor, so that can create a parser instance. +/// +public abstract class CustomTypeParser : BaseParser where T : notnull; + +/// +/// Inheritors of this class are used as the default parsers when trying to resolve arguments of type . +/// Inheritors need to have a parameterless constructor, so that can create a parser +/// instance. +/// +public abstract class TypeParser : BaseParser where T : notnull; + +/// +/// Base class for custom parsers that exist simply to provide customized auto-completion options. +/// +public abstract class CustomCompletionParser : CustomTypeParser where T : notnull +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out T? result) + { + // Use default parser type T + return Toolshed.TryParse(ctx, out result); + } } diff --git a/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs index 243301abf..18284d83f 100644 --- a/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/TypeTypeParser.cs @@ -19,7 +19,7 @@ namespace Robust.Shared.Toolshed.TypeParsers; // TODO: This should be able to parse more types, currently it only knows the ones in SimpleTypes. -internal sealed class TypeTypeParser : TypeParser +public sealed class TypeTypeParser : TypeParser { [Dependency] private readonly IModLoader _modLoader = default!; @@ -79,12 +79,12 @@ internal sealed class TypeTypeParser : TypeParser _optionsCache = CompletionResult.FromHintOptions(Types.Select(x => new CompletionOption(x.Key)), "C# level type"); } - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) { - var firstWord = parserContext.GetWord(Rune.IsLetterOrDigit); + var firstWord = ctx.GetWord(Rune.IsLetterOrDigit); if (firstWord is null) { - error = new OutOfInputError(); + ctx.Error = new OutOfInputError(); result = null; return false; } @@ -93,16 +93,16 @@ internal sealed class TypeTypeParser : TypeParser if (ty is null) { - error = new UnknownType(firstWord); + ctx.Error = new UnknownType(firstWord); result = null; return false; } if (ty.IsGenericTypeDefinition) { - if (!parserContext.EatMatch('<')) + if (!ctx.EatMatch('<')) { - error = new ExpectedGeneric(); + ctx.Error = new ExpectedGeneric(); result = null; return false; } @@ -112,25 +112,25 @@ internal sealed class TypeTypeParser : TypeParser for (var i = 0; i < len; i++) { - if (!TryParse(parserContext, out var t, out error)) + if (!TryParse(ctx, out var t)) { result = null; return false; } - args[i] = (Type) t; + args[i] = t; - if (i != (len - 1) && !parserContext.EatMatch(',')) + if (i != (len - 1) && !ctx.EatMatch(',')) { - error = new ExpectedNextType(); + ctx.Error = new ExpectedNextType(); result = null; return false; } } - if (!parserContext.EatMatch('>')) + if (!ctx.EatMatch('>')) { - error = new ExpectedGeneric(); + ctx.Error = new ExpectedGeneric(); result = null; return false; } @@ -138,11 +138,11 @@ internal sealed class TypeTypeParser : TypeParser ty = ty.MakeGenericType(args); } - if (parserContext.EatMatch('[')) + if (ctx.EatMatch('[')) { - if (!parserContext.EatMatch(']')) + if (!ctx.EatMatch(']')) { - error = new UnknownType(firstWord); + ctx.Error = new UnknownType(firstWord); result = null; return false; } @@ -150,13 +150,12 @@ internal sealed class TypeTypeParser : TypeParser ty = ty.MakeArrayType(); } - if (parserContext.EatMatch('?') && (ty.IsValueType || ty.IsPrimitive)) + if (ctx.EatMatch('?') && (ty.IsValueType || ty.IsPrimitive)) { ty = typeof(Nullable<>).MakeGenericType(ty); } result = ty; - error = null; return true; } @@ -166,11 +165,11 @@ internal sealed class TypeTypeParser : TypeParser return ty; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - // TODO: Suggest generics. - return ValueTask.FromResult<(CompletionResult? result, IConError? error)>((_optionsCache, null)); + // TODO TOOLSHED Generic Type Suggestions. + return _optionsCache; } } @@ -209,16 +208,3 @@ public record struct UnknownType(string T) : IConError public Vector2i? IssueSpan { get; set; } public StackTrace? Trace { get; set; } } - - -internal record struct TypeIsSandboxViolation(Type T) : IConError -{ - public FormattedMessage DescribeInner() - { - return FormattedMessage.FromUnformatted($"The type {T.PrettyName()} is not permitted under sandbox rules."); - } - - public string? Expression { get; set; } - public Vector2i? IssueSpan { get; set; } - public StackTrace? Trace { get; set; } -} diff --git a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs index 85e691f75..276a7af83 100644 --- a/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs +++ b/Robust.Shared/Toolshed/TypeParsers/ValueRefTypeParser.cs @@ -1,116 +1,139 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Threading.Tasks; +using System.Text; using Robust.Shared.Console; -using Robust.Shared.IoC; -using Robust.Shared.Log; using Robust.Shared.Toolshed.Errors; using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; + +#pragma warning disable CS0618 // Type or member is obsolete namespace Robust.Shared.Toolshed.TypeParsers; -internal sealed class ValueRefTypeParser : TypeParser> +internal sealed class ValueRefTypeParser : TypeParser> { - [Dependency] private readonly ToolshedManager _toolshed = default!; - - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRef? result) { - var res = _toolshed.TryParse>(parserContext, out var inner, out error); + var res = Toolshed.TryParse>(ctx, out var inner); result = null; if (res) - result = new ValueRef((ValueRef)inner!); + result = new ValueRef(inner!); return res; } - public override ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, string? argName) + public override CompletionResult? TryAutocomplete(ParserContext parserContext, string? argName) { - return _toolshed.TryAutocomplete(parserContext, typeof(ValueRef), argName); + return Toolshed.TryAutocomplete(parserContext, typeof(ValueRef), argName); } } -internal sealed class VarRefParser : TypeParser> +internal sealed class ValueRefTypeParser : TypeParser> { - [Dependency] private readonly ToolshedManager _toolshed = default!; - - public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] out object? result, out IConError? error) + internal static bool TryParse( + ToolshedManager shed, + ParserContext ctx, + ITypeParser? parser, + [NotNullWhen(true)] out ValueRef? result) { - error = null; - parserContext.ConsumeWhitespace(); + result = null; - if (parserContext.EatMatch('$')) + ctx.ConsumeWhitespace(); + var rune = ctx.PeekRune(); + if (rune == new Rune('$')) { - // We're parsing a variable. - if (parserContext.GetWord(ParserContext.IsToken) is not { } word) - { - error = new OutOfInputError(); - result = null; + if (!shed.TryParse>(ctx, out var valRef)) return false; - } - result = new ValueRef(word); - error = null; + result = valRef; return true; } - else + + if (rune == new Rune('{')) { - var chkpoint = parserContext.Save(); - if (Block.TryParse(false, parserContext, null, out var block, out _, out error)) - { - result = new ValueRef(block); - return true; - } - parserContext.Restore(chkpoint); + if (!shed.TryParse>(ctx, out var block)) + return false; - var success = _toolshed.TryParse(parserContext, out var value, out error); + result = new BlockRef(block); + return true; + } - if (error is UnparseableValueError) - error = null; - - if (value is not null && success) - { - result = new ValueRef((T)value); - error = null; - return true; - } - - - result = null; + parser ??= shed.GetParserForType(typeof(T)); + if (parser == null) + { + // No parser is available -> must be provided via a variable or block + if (!ctx.GenerateCompletions) + ctx.Error = new MustBeVarOrBlock(typeof(T)); return false; } + + if (!parser.TryParse(ctx, out var obj)) + return false; + + if (obj is not T value) + return false; + + result = new ParsedValueRef(value); + return true; } - public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext, - string? argName) + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRef? result) { - parserContext.ConsumeWhitespace(); + return TryParse(Toolshed, ctx, null, out result); + } - if (parserContext.EatMatch('$')) + public static CompletionResult? TryAutocomplete( + ToolshedManager shed, + ParserContext ctx, + string? argName, + ITypeParser? parser) + { + ctx.ConsumeWhitespace(); + var rune = ctx.PeekRune(); + if (rune == new Rune('$')) + return shed.TryAutocomplete(ctx, typeof(VarRef), argName); + + if (rune == new Rune('{')) { - return (CompletionResult.FromHint(""), null); + Block.TryParse(ctx, out _); + return ctx.Completions; } - else - { - var chkpoint = parserContext.Save(); - var (res, err) = await _toolshed.TryAutocomplete(parserContext, typeof(TAuto), null); - parserContext.Restore(chkpoint); - CompletionOption[] parseOptions = Array.Empty(); - if (err is not UnparseableValueError || res is not null) - { - parseOptions = res?.Options ?? parseOptions; - } - chkpoint = parserContext.Save(); - Block.TryParse(true, parserContext, null, out _, out var result, out _); - if (result is not null) - { - var (blockRes, _) = await result.Value; - var options = blockRes?.Options ?? Array.Empty(); - return (CompletionResult.FromHintOptions(parseOptions.Concat(options).ToArray(), $""), err); - } - parserContext.Restore(chkpoint); + parser ??= shed.GetParserForType(typeof(T)); - return (CompletionResult.FromHint("$"), null); - } + if (parser == null) + return CompletionResult.FromHint($""); + + var res = parser.TryAutocomplete(ctx, null); + return res ?? CompletionResult.FromHint($""); + } + + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + return TryAutocomplete(Toolshed, ctx, argName, null); + } +} + +internal sealed class CustomValueRefTypeParser : CustomTypeParser> + where TParser : CustomTypeParser, new() + where T : notnull +{ + public override CompletionResult? TryAutocomplete(ParserContext ctx, string? argName) + { + var parser = Toolshed.GetCustomParser(); + return ValueRefTypeParser.TryAutocomplete(Toolshed, ctx, argName, parser); + } + + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out ValueRef? result) + { + var parser = Toolshed.GetCustomParser(); + return ValueRefTypeParser.TryParse(Toolshed, ctx, parser, out result); + } +} + +public sealed class MustBeVarOrBlock(Type T) : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Command expects an argument of type {T.PrettyName()}.\nHowever this type has no parser available, and thus cannot be directly parsed.\nInstead, you have to use a variable or command block to provide it."); } } diff --git a/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs new file mode 100644 index 000000000..9c99d64b1 --- /dev/null +++ b/Robust.Shared/Toolshed/TypeParsers/VarRefType.cs @@ -0,0 +1,72 @@ +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Console; +using Robust.Shared.Maths; +using Robust.Shared.Toolshed.Commands.Values; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; + +namespace Robust.Shared.Toolshed.TypeParsers; + +/// +/// This custom parser is for parsing the type of a . I.e., this can be used if the type of a +/// toolshed variable should be used as the generic argument for a command. Note that this will not actually "eat" a +/// successfully parsed the , such that it can be used as a command argument. However, this +/// also means that this must always be the last type argument, and the first command argument. +/// +/// +/// Note that this uses to determine the variable type. If a variable's +/// type is modified during a command invocation in a way that this parser was not not aware of, this may result in +/// a . +/// +public sealed class VarTypeParser : CustomTypeParser +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Type? result) + { + result = null; + var save = ctx.Save(); + + ctx.ConsumeWhitespace(); + if (!ctx.EatMatch('$')) + return false; + + var name = ctx.GetWord(ParserContext.IsToken); + + if (string.IsNullOrEmpty(name)) + { + if (!ctx.GenerateCompletions) + ctx.Error = new OutOfInputError(); + return false; + } + + if (ctx.VariableParser.TryParseVar(name, out result)) + { + // Reset the parser, so the VarRef can be parsed as a command argument. + ctx.Restore(save); + return true; + } + + if (!ctx.GenerateCompletions) + ctx.Error = new UnknownVariableError(name); + return false; + } + + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + { + return ctx.VariableParser.GenerateCompletions(); + } +} + +public record UnknownVariableError(string VarName) : IConError +{ + public FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted($"Unknown variable '${VarName}'. Cannot infer type. Consider using {nameof(ValCommand)} and explicitly specifying the type."); + } + + public string? Expression { get; set; } + public Vector2i? IssueSpan { get; set; } + public StackTrace? Trace { get; set; } +} diff --git a/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs b/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs new file mode 100644 index 000000000..c6251468b --- /dev/null +++ b/Robust.Shared/Toolshed/TypeParsers/VarRefTypeParser.cs @@ -0,0 +1,91 @@ +using System.Diagnostics.CodeAnalysis; +using Robust.Shared.Console; +using Robust.Shared.Toolshed.Errors; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Utility; + +namespace Robust.Shared.Toolshed.TypeParsers; + +internal sealed class VarRefTypeParser : TypeParser> +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out VarRef? result) + { + result = null; + + ctx.ConsumeWhitespace(); + var start = ctx.Index; + if (!ctx.EatMatch('$')) + { + if (ctx.GenerateCompletions) + return false; + ctx.Error = new ExpectedDollarydoo(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); + return false; + } + + start = ctx.Index; + var name = ctx.GetWord(ParserContext.IsToken); + + if (string.IsNullOrEmpty(name)) + { + if (ctx.GenerateCompletions) + return false; + ctx.Error = new ExpectedVariableName(); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); + return false; + } + + result = new VarRef(name); + return true; + } + + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + { + return parserContext.VariableParser.GenerateCompletions(); + } +} + +public sealed class WriteableVarRefParser : TypeParser> +{ + public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out WriteableVarRef? result) + { + var start = ctx.Index; + result = null; + if (!ctx.Toolshed.TryParse(ctx, out VarRef? inner)) + return false; + + if (!ctx.VariableParser.IsReadonlyVar(inner.VarName)) + { + result = new(inner); + return true; + } + if (ctx.GenerateCompletions) + return false; + + ctx.Error = new ReadonlyVariableError(inner.VarName); + ctx.Error.Contextualize(ctx.Input, (start, ctx.Index+1)); + return false; + } + + public override CompletionResult TryAutocomplete(ParserContext parserContext, string? argName) + { + return parserContext.VariableParser.GenerateCompletions(false); + } +} + + +public sealed class ExpectedDollarydoo : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted("Expected a variable name, which must start with a '$'."); + } +} + +public sealed class ExpectedVariableName : ConError +{ + public override FormattedMessage DescribeInner() + { + return FormattedMessage.FromUnformatted("Expected a valid variable name."); + } +} diff --git a/Robust.Shared/Utility/TypeHelpers.cs b/Robust.Shared/Utility/TypeHelpers.cs index 52f789c7a..8831e60fa 100644 --- a/Robust.Shared/Utility/TypeHelpers.cs +++ b/Robust.Shared/Utility/TypeHelpers.cs @@ -186,6 +186,11 @@ namespace Robust.Shared.Utility return memberInfo.GetCustomAttribute() != null; } + public static bool HasCustomAttribute(this ParameterInfo memberInfo) where T : Attribute + { + return memberInfo.GetCustomAttribute() != null; + } + public static bool TryGetCustomAttribute(this MemberInfo memberInfo, [NotNullWhen(true)] out T? attribute) where T : Attribute { diff --git a/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs b/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs index 2fb841e59..d8cf82da7 100644 --- a/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs +++ b/Robust.UnitTesting/Shared/GameObjects/GenericEntityPrint.cs @@ -167,7 +167,7 @@ public sealed class GenericEntityPrint } structs.Append($$""" - public record struct Entity<{{generics}}> + public record struct Entity<{{generics}}> : IFluentEntityUid, IAsType {{constraints.ToString().TrimEnd()}} { public EntityUid Owner; @@ -203,6 +203,9 @@ public sealed class GenericEntityPrint {{deConstructorAccess.ToString().TrimEnd()}} } {{castRegion}} + + EntityUid IFluentEntityUid.FluentOwner => Owner; + public EntityUid AsType() => Owner; } diff --git a/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs b/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs index 1ec829f4a..660db2a48 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ArithmeticTest.cs @@ -63,7 +63,7 @@ public sealed class ArithmeticTest : ToolshedTest list.Add(i + 1); } - Assert.That(list, Is.EquivalentTo(InvokeCommand>("f 0 iterate + 1 100"))); + Assert.That(list, Is.EquivalentTo(InvokeCommand>("f 0 iterate { + 1 } 100"))); }); } diff --git a/Robust.UnitTesting/Shared/Toolshed/LocTest.cs b/Robust.UnitTesting/Shared/Toolshed/LocTest.cs index c47ea04a5..4653556a2 100644 --- a/Robust.UnitTesting/Shared/Toolshed/LocTest.cs +++ b/Robust.UnitTesting/Shared/Toolshed/LocTest.cs @@ -2,7 +2,6 @@ using System.Globalization; using System.Threading.Tasks; using NUnit.Framework; -using Robust.Shared.IoC; using Robust.Shared.Localization; using Robust.Shared.Toolshed; @@ -14,12 +13,32 @@ public sealed class LocTest : ToolshedTest [Test] public async Task AllCommandsHaveDescriptions() { + var locMan = Server.ResolveDependency(); + var toolMan = Server.ResolveDependency(); + var locStrings = new HashSet(); + + // Its neat that you can mostly do this via toolshed and all, but I'm still gonna turn it into a "real" test. + // Assert.That(InvokeCommand("cmd:list where { cmd:descloc loc:tryloc isnull }", out var res)); + // Assert.That((IEnumerable)res!, Is.Empty, "All commands must have localized descriptions."); + + var testAssembly = typeof(LocTest).Assembly; + await Server.WaitAssertion(() => { - IoCManager.Resolve().LoadCulture(new CultureInfo("en-US")); + locMan.LoadCulture(new CultureInfo("en-US")); - Assert.That(InvokeCommand("cmd:list where { cmd:descloc loc:tryloc isnull }", out var res)); - Assert.That((IEnumerable)res!, Is.Empty, "All commands must have localized descriptions."); + Assert.Multiple(() => + { + foreach (var cmd in toolMan.DefaultEnvironment.AllCommands()) + { + if (cmd.Cmd.GetType().Assembly == testAssembly) + continue; + + var descLoc = cmd.DescLocStr(); + Assert.That(locStrings.Add(descLoc), $"Duplicate command description key: {descLoc}"); + Assert.That(locMan.TryGetString(descLoc, out _), $"Failed to get command description for command {cmd.FullName()}"); + } + }); }); } } diff --git a/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs new file mode 100644 index 000000000..8f45915c7 --- /dev/null +++ b/Robust.UnitTesting/Shared/Toolshed/TestCommands.cs @@ -0,0 +1,90 @@ +using System; +using Robust.Shared.Console; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; + +namespace Robust.UnitTesting.Shared.Toolshed; + +// This file just contains a collection of various test commands for use in other tests. + +[ToolshedCommand] +public sealed class TestVoidCommand : ToolshedCommand +{ + [CommandImplementation] public void Impl() {} +} + +[ToolshedCommand] +public sealed class TestIntCommand : ToolshedCommand +{ + [CommandImplementation] public int Impl() => 1; +} + + +[ToolshedCommand] +public sealed class TestTypeArgCommand : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation] public string Impl() => typeof(T).Name; +} + +[ToolshedCommand] +public sealed class TestMultiTypeArgCommand : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; + [CommandImplementation] public string Impl(int i) + => $"{typeof(T1).Name}, {typeof(T2).Name}, {i}"; +} + +[ToolshedCommand] +public sealed class TestIntStrArgCommand : ToolshedCommand +{ + [CommandImplementation] public int Impl(int i, string str) => i; +} + +[ToolshedCommand] +public sealed class TestPipedIntCommand : ToolshedCommand +{ + [CommandImplementation] public int Impl([PipedArgument] int i) => i; +} + +[ToolshedCommand] +public sealed class TestCustomVarRefParserCommand : ToolshedCommand +{ + [CommandImplementation] + public int Impl([CommandArgument(typeof(Parser))] int i) => i; + + [Virtual] + public class Parser : CustomTypeParser + { + public override bool TryParse(ParserContext ctx, out int result) + { + result = default; + if (!Toolshed.TryParse(ctx, out int _)) + return false; + + // Disregard the parsed value and always return 1 + result = 1; + return true; + } + + public override CompletionResult TryAutocomplete(ParserContext ctx, string? argName) + { + return new CompletionResult([new("A")], "B"); + } + } +} + +[ToolshedCommand] +public sealed class TestCustomParserCommand : ToolshedCommand +{ + [CommandImplementation] + public int Impl([CommandArgument(typeof(Parser))] int i) => i; + + public sealed class Parser : TestCustomVarRefParserCommand.Parser + { + // Disable ValueRef support. + // I.e., this parser will not not try to parse variables or blocks + public override bool EnableValueRef => false; + } +} diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs index 5130bf152..b444289f7 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Bugcheck.cs @@ -1,7 +1,5 @@ -using System; -using System.Threading.Tasks; +using System.Threading.Tasks; using NUnit.Framework; -using Robust.Shared.Maths; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Toolshed.TypeParsers.Math; @@ -16,20 +14,7 @@ public sealed partial class ToolshedParserTest [Test, TestOf(typeof(Quantity))] public async Task Bug_QuantityPercentage_BeforeTime() { - await Server.WaitAssertion(() => - { - ParseCommand("val Quantity 50%"); - }); - } - - // Weird parsing issue around overly deep nesting. - [Test, TestOf(typeof(Block))] - public async Task Bug_DeepNest_08_21_2023() - { - await Server.WaitAssertion(() => - { - ParseCommand("f 100 iota map { iota sum emplace { f 2 pow $value } }"); - }); + await Server.WaitAssertion(() => AssertResult("val Quantity 50%", new Quantity(null, 0.5f))); } // Toolshed outputting the wrong error kind here, it should not be an unknown command error. @@ -38,19 +23,7 @@ public sealed partial class ToolshedParserTest { await Server.WaitAssertion(() => { - ExpectError(); - ParseCommand("val Color 180deg"); - }); - } - - // Terminator parsing would try to eat an entire word instead of only the terminator. - // This was fixed with the introduction of TryMatch. - [Test, TestOf(nameof(ParserContext.EatTerminator)), TestOf(nameof(ParserContext.TryMatch))] - public async Task Bug_TerminatorSpacing_08_23_2023() - { - await Server.WaitAssertion(() => - { - ParseCommand("f 100 iota map {iota sum emplace {f 2 pow $value}}"); + ParseError("val Color 180deg"); }); } } diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs index 238f22510..824c82730 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.Core.cs @@ -67,7 +67,6 @@ public sealed partial class ToolshedParserTest // Toolshed special constructs. AssertParseable>(); - AssertParseable>(); AssertParseable(); AssertParseable>(); AssertParseable>(); diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs index 59c54c71e..e966fe023 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedParserTest.cs @@ -1,7 +1,10 @@ using System.Collections.Generic; +using System.Numerics; using System.Threading.Tasks; using NUnit.Framework; using Robust.Shared.GameObjects; +using Robust.Shared.Maths; +using Robust.Shared.Toolshed; using Robust.Shared.Toolshed.Syntax; using Robust.Shared.Toolshed.TypeParsers; using Robust.Shared.Toolshed.TypeParsers.Math; @@ -20,13 +23,11 @@ public sealed partial class ToolshedParserTest : ToolshedTest ParseCommand("entities select 1"); ParseCommand("entities with MetaData select 1"); - ExpectError(); - ParseCommand("entities with"); + ParseError("entities with"); - ExpectError(); - ParseCommand("player:list with MetaData"); + ParseError("player:list with MetaData"); - ExpectError(); + ExpectError(); ParseCommand("player:list", expectedType: typeof(IEnumerable)); ParseCommand("entities not with MetaData"); @@ -45,8 +46,7 @@ public sealed partial class ToolshedParserTest : ToolshedTest ParseCommand("ent 1"); // Clientside entities are a myth. - ExpectError(); - ParseCommand("ent foodigity"); + ParseError("ent foodigity"); }); } @@ -57,15 +57,9 @@ public sealed partial class ToolshedParserTest : ToolshedTest { ParseCommand("entities select 100"); ParseCommand("entities select 50%"); - - ExpectError(); - ParseCommand("entities select -1"); - - ExpectError(); - ParseCommand("entities select 200%"); - - ExpectError(); - ParseCommand("entities select hotdog"); + ParseError("entities select -1"); + ParseError("entities select 200%"); + ParseError("entities select hotdog"); }); } @@ -75,12 +69,8 @@ public sealed partial class ToolshedParserTest : ToolshedTest await Server.WaitAssertion(() => { ParseCommand("entities with MetaData"); - - ExpectError(); - ParseCommand("entities with Foodiddy"); - - ExpectError(); - ParseCommand("entities with MetaDataComponent"); + ParseError("entities with Foodiddy"); + ParseError("entities with MetaDataComponent"); }); } @@ -89,12 +79,11 @@ public sealed partial class ToolshedParserTest : ToolshedTest { await Server.WaitAssertion(() => { - ParseCommand("val Color red"); - ParseCommand("val Color blue"); - ParseCommand("val Color green"); - ParseCommand("val Color #FF0000"); - ParseCommand("val Color #00FF00"); - ParseCommand("val Color #0000FF"); + AssertResult("val Color red", Color.Red); + AssertResult("val Color blue", Color.Blue); + AssertResult("val Color green", Color.Green); + AssertResult("val Color #89ABCD", Color.FromHex("#89ABCD")); + AssertResult("val Color #89ABCDEF", Color.FromHex("#89ABCDEF")); }); } @@ -103,8 +92,8 @@ public sealed partial class ToolshedParserTest : ToolshedTest { await Server.WaitAssertion(() => { - ParseCommand("val Angle 3.14159"); - ParseCommand("val Angle 180deg"); + AssertResult("val Angle 3.14159", new Angle(3.14159f)); + AssertResult("val Angle 180deg", Angle.FromDegrees(180)); }); } @@ -113,19 +102,13 @@ public sealed partial class ToolshedParserTest : ToolshedTest { await Server.WaitAssertion(() => { - ParseCommand("val Vector2 [1, 1]"); - ParseCommand("val Vector2 [-1, 1]"); - ParseCommand("val Vector2 [ 1 , 1 ]"); - ParseCommand("val Vector2 [ -1, 1 ]"); - - ExpectError(); - ParseCommand("val Vector2 1, 1"); - - ExpectError(); - ParseCommand("val Vector2 [1, 1"); - - ExpectError(); - ParseCommand("val Vector2 [1]"); + AssertResult("val Vector2 [1, 1]", new Vector2(1, 1)); + AssertResult("val Vector2 [-1, 1]", new Vector2(-1, 1)); + AssertResult("val Vector2 [ 1 , 1 ]", new Vector2(1, 1)); + AssertResult("val Vector2 [ -1, 1 ]", new Vector2(-1, 1)); + ParseError("val Vector2 1, 1"); + ParseError("val Vector2 [1, 1"); + ParseError("val Vector2 [1]"); }); } } diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs index 2e77af544..7d9574fe1 100644 --- a/Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTest.cs @@ -1,7 +1,10 @@ using System; +using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; +using Robust.Shared.Console; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Toolshed; @@ -23,8 +26,6 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext protected IInvocationContext? InvocationContext = null; - - [TearDown] public async Task TearDownInternal() { @@ -34,7 +35,7 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext protected virtual Task TearDown() { - Assert.That(_expectedErrors, Is.Empty); + Assert.That(ExpectedErrors, Is.Empty); ClearErrors(); return Task.CompletedTask; } @@ -58,37 +59,147 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext return Toolshed.InvokeCommand(this, command, null, out result); } + protected CompletionResult? GetCompletions(string cmd) + { + return Toolshed.GetCompletions(this, cmd); + } + + protected object? InvokeCommand(string command, Type output) + { + Assert.That(InvokeCommand(command, out var res)); + Assert.That(res, Is.AssignableTo(output)); + return res; + } + protected T InvokeCommand(string command) { - InvokeCommand(command, out var res); - Assert.That(res, Is.AssignableTo()); - return (T) res!; + return (T) InvokeCommand(command, typeof(T))!; + } + + protected object? InvokeCommand(string command, TIn input, Type output) + { + Assert.That(Toolshed.InvokeCommand(this, command, input, out var res)); + Assert.That(res, Is.AssignableTo(output)); + return res; } protected TOut InvokeCommand(string command, TIn input) { - Toolshed.InvokeCommand(this, command, input, out var res); - Assert.That(res, Is.AssignableTo()); - return (TOut) res!; + return (TOut) InvokeCommand(command, input, typeof(TOut))!; } - protected ParserContext Parser(string input) => new ParserContext(input, Toolshed); + protected void AssertResult(string command, object? expected) + { + Assert.That(InvokeCommand(command, out var result)); + if (expected is IEnumerable @enum) + Assert.That(result, Is.EquivalentTo(@enum)); + else + Assert.That(result, Is.EqualTo(expected)); + } protected void AssertParseable() { - Toolshed.TryParse(Parser(""), out _, out var err); - Assert.That(err, Is.Not.TypeOf(), $"Couldn't find a parser for {typeof(T).PrettyName()}"); + var parser = new ParserContext("", Toolshed, Toolshed.DefaultEnvironment, IVariableParser.Empty, null); + Toolshed.TryParse(parser, out _); + Assert.That(parser.Error, Is.Not.TypeOf(), $"Couldn't find a parser for {typeof(T).PrettyName()}"); } - protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null, bool once = false) + #region Completion Asserts + + protected void AssertCompletion(string command, CompletionResult? expected) { - var parser = new ParserContext(command, Toolshed); - var success = CommandRun.TryParse(false, parser, inputType, expectedType, once, out _, out _, out var error); + var result = GetCompletions(command); + if (expected == null) + { + Assert.That(result, Is.Null); + return; + } - if (error is not null) - ReportError(error); + Assert.That(result, Is.Not.Null); + if (result == null) + return; - if (error is null) + Assert.That(result.Hint, Is.EqualTo(expected.Hint)); + Assert.That(result.Options, Is.EquivalentTo(expected.Options)); + } + + protected void AssertCompletionEmpty(string command) + { + var result = GetCompletions(command); + if (result == null) + return; + + Assert.That(result.Options.Length, Is.EqualTo(0)); + Assert.That(result.Hint, Is.Null); + } + + protected void AssertCompletionHint(string command, string hint) + { + var result = GetCompletions(command); + Assert.That(result, Is.Not.Null); + if (result == null) + return; + + Assert.That(result.Hint, Is.EqualTo(hint)); + } + + protected void AssertCompletionSingle(string command, string expected) + { + var result = GetCompletions(command); + Assert.That(result, Is.Not.Null); + if (result == null) + return; + + Assert.That(result.Options.Length, Is.EqualTo(1)); + if (result.Options.Length != 1) + return; + + Assert.That(result.Options[0].Value, Is.EqualTo(expected)); + } + + protected void AssertCompletionContains(string command, params string[] expected) + { + var result = GetCompletions(command); + + Assert.That(result, Is.Not.Null); + if (result == null) + return; + + Assert.That(result.Options.Length, Is.GreaterThanOrEqualTo(expected.Length)); + if (result.Options.Length != 1) + return; + + foreach (var ex in expected) + { + Assert.That(result.Options.Any(x => x.Value == ex)); + } + } + + /// + /// Check that the given string is not a suggested completion option. + /// + protected void AssertCompletionInvalid(string command, string invalid) + { + var result = GetCompletions(command); + if (result == null) + return; + + foreach (var res in result.Options) + { + Assert.That(res.Value, Is.Not.EqualTo(invalid)); + } + } + + + #endregion + + protected void ParseCommand(string command, Type? inputType = null, Type? expectedType = null) + { + var parser = new ParserContext(command, Toolshed, Toolshed.DefaultEnvironment, IVariableParser.Empty, null); + var success = CommandRun.TryParse(parser, inputType, expectedType, out _); + ReportError(parser.Error); + + if (parser.Error is null) Assert.That(success, $"Parse failed despite no error being reported. Parsed {command}"); } @@ -125,13 +236,20 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext return; } - private Queue _expectedErrors = new(); + protected Queue ExpectedErrors = new(); - private List _errors = new(); + protected List Errors = new(); - public void ReportError(IConError err) + public void ReportError(IConError? err) { - if (_expectedErrors.Count == 0) + if (err == null) + { + if (ExpectedErrors.Count > 0) + Assert.Fail($"Expected an error of type {ExpectedErrors.Peek().PrettyName()}, but none was received"); + return; + } + + if (ExpectedErrors.Count == 0) { if (AssertOnUnexpectedError) { @@ -141,7 +259,7 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext goto done; } - var ty = _expectedErrors.Dequeue(); + var ty = ExpectedErrors.Dequeue(); if (AssertOnUnexpectedError) { @@ -152,28 +270,54 @@ public abstract class ToolshedTest : RobustIntegrationTest, IInvocationContext } done: - _errors.Add(err); + Errors.Add(err); } public IEnumerable GetErrors() { - return _errors; + return Errors; } + public bool HasErrors => Errors.Count > 0; + public void ClearErrors() { - _errors.Clear(); + Errors.Clear(); + } + + /// + public object? ReadVar(string name) + { + return Variables.GetValueOrDefault(name); + } + + /// + public void WriteVar(string name, object? value) + { + Variables[name] = value; + } + + /// + public IEnumerable GetVars() + { + return Variables.Keys; } public Dictionary Variables { get; } = new(); protected void ExpectError(Type err) { - _expectedErrors.Enqueue(err); + ExpectedErrors.Enqueue(err); } - protected void ExpectError() + protected void ExpectError() where T : IConError { - _expectedErrors.Enqueue(typeof(T)); + ExpectError(typeof(T)); + } + + protected void ParseError(string cmd) where T : IConError + { + ExpectError(); + ParseCommand(cmd); } } diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs new file mode 100644 index 000000000..23746e022 --- /dev/null +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedTests.cs @@ -0,0 +1,298 @@ +using System; +using System.Threading.Tasks; +using NUnit.Framework; +using Robust.Shared.GameObjects; +using Robust.Shared.Player; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.Commands.Generic; +using Robust.Shared.Toolshed.Syntax; +using Robust.Shared.Toolshed.TypeParsers; +using Robust.Shared.Toolshed.TypeParsers.Math; + +namespace Robust.UnitTesting.Shared.Toolshed; + +/// +/// Collection of miscellaneous toolshed command tests. +/// Several of these were just added ad hoc as bugs arose. +/// +public sealed class ToolshedTests : ToolshedTest +{ + // TODO Robust.UnitTesting + // split these into separate [TestCase]s when we have pooling. + // Or some other way to avoid starting a server per command. + [Test] + public async Task TestMiscCommands() + { + await Server.WaitAssertion(() => + { + AssertResult("i 5 iota reduce { max $value }", 5); + AssertResult("i 5 iota sum", 15); + AssertResult("i 5 iota map { + 1 }", new[] {2, 3, 4, 5, 6}); + AssertResult("f 5 iota map { iota sum emplace { f 2 pow $value } }", new[] {2.0f, 8.0f, 64.0f, 1024.0f, 32768.0f}); + AssertResult("f 5 iota map {iota sum emplace {f 2 pow $value}}", new[] {2.0f, 8.0f, 64.0f, 1024.0f, 32768.0f}); + AssertResult("i 0 map { + 1 }", new [] { 1 }); + AssertResult("i 1 to 1", new [] { 1 }); + AssertResult("f 1 to 1", new [] { 1 }); + AssertResult("i -2 to 2 map { + 1 }", new [] { -1, 0, 1, 2, 3 }); + AssertResult("f -2 to 2 map { + 1 }", new [] { -1, 0, 1, 2, 3 }); + AssertResult("f 1 to 1", new [] { 1 }); + AssertResult("i 2 + 2", 4); + AssertResult("i 2 + { i 2 }", 4); + AssertResult("i 2 + 2 * 2", 8); + AssertResult("i 2 + { i 2 * 2 }", 6); + AssertResult("i 3 iota max { i 3 iota }", new[] {1, 2, 3 }); + AssertResult("i 1 iota iterate { take 1 } 3", new[] { new[]{1}, [1], [1] }); + AssertResult("i 3 iota iterate { take 2 } 3", new[] { new[]{1, 2}, [1, 2], [1, 2] }); + + ParseError(""); + ParseError(" "); + ParseError("{"); + ParseError("}"); + ParseError("{}"); + ParseError(";"); + ParseError("i 2 + { }"); + ParseError("i 3 iota max 3"); + ParseError("i 1 iota iterate { average } 2"); + + // Meta-test: check that ParseError() fails if the parse actually succeeds without generating an error + Assert.Throws(() => ParseError("i 2")); + Assert.That(ExpectedErrors.Count, Is.EqualTo(1)); + ExpectedErrors.Clear(); + }); + } + + [Test] + public async Task TestTypeArgs() + { + await Server.WaitAssertion(() => + { + AssertResult("testtypearg int", "Int32"); + AssertResult("testmultitypearg float string -1", "Single, String, -1"); + + ParseError("testtypearg"); + ParseError("testtypearg "); + ParseError("testtypearg invalidType"); + ParseError("testmultitypearg int"); + ParseError("testmultitypearg int "); + ParseError("testmultitypearg int invalidType"); + ParseError("testmultitypearg int float"); + ParseError("testmultitypearg int float "); + }); + } + + [Test] + public async Task TestCommandTerminator() + { + await Server.WaitAssertion(() => + { + // Terminators can be used to chain together commands that output void + AssertResult("testvoid", null); + ParseError("testvoid testvoid"); + AssertResult("testvoid;testvoid", null); + AssertResult("testvoid; testvoid", null); + AssertResult("testvoid ; testvoid", null); + AssertResult("testvoid ;; ; testvoid", null); + + // Terminators allow commands that output data to be chained with commands that take no inputs + AssertResult("testint", 1); + AssertResult("testint; testint", 1); + AssertResult("testvoid; testint", 1); + AssertResult("testint; testvoid", null); + + // Terminators can interrupt argument parsing + AssertResult("testintstrarg 1 \"A\"", 1); + AssertResult("testintstrarg 1 \"A\"; testvoid", null); + ParseError("testintstrarg 1;"); + ParseError("testintstrarg 1; \"A\""); + ParseError("testintstrarg 1; testvoid"); + ParseError("testintstrarg 1; \"A\"; testvoid"); + + // Terminators can interrupt type-argument parsing + AssertResult("testmultitypearg float string -1", "Single, String, -1"); + AssertResult("testmultitypearg float string -1; testvoid", null); + ParseError("testmultitypearg float;"); + ParseError("testmultitypearg float; string -1"); + ParseError("testmultitypearg float; testvoid"); + ParseError("testmultitypearg float string; testvoid"); + + // Terminators don't actually discard the final output type if it is the end of the command.; + AssertResult("testint;", 1); + AssertResult("testint; testint;", 1); + AssertResult("i 2 + { i 2; }", 4); + AssertResult("i 2 + { i 2; ; } ;; ;", 4); + }); + } + + [Test] + public async Task TestVariables() + { + await Server.WaitAssertion(() => + { + AssertResult("i 2 => $x", 2); + AssertResult("val int $x", 2); + AssertResult("var $x", 2); + AssertResult("i 2 + $x", 4); + AssertResult("i 2 + $x + 2 ", 6); + AssertResult("i 2 + { i 2 * $x }", 6); + AssertResult("i 5 iota emplace { val int $value }", new[] {1, 2, 3, 4, 5}); + AssertResult("i 5 iota emplace { var $value }", new[] {1, 2, 3, 4, 5}); + AssertResult("i 5 iota emplace { i 1 + $value }", new[] {2, 3, 4, 5, 6}); + AssertResult("i 5 iota reduce { + $value }", 15); + }); + } + + [Test, TestOf(typeof(EmplaceCommand))] + public async Task TestEmplace() + { + await Server.WaitAssertion(() => + { + + var ent = Server.EntMan.Spawn(); + Server.System().SetEntityName(ent, "Foo"); + + AssertResult($"ent e{ent.Id} emplace {{ val EntityUid $value }}", ent); + AssertResult($"ent e{ent.Id} emplace {{ var $value }}", ent); + AssertResult($"ent e{ent.Id} emplace {{ f 2 + $wx }}", 2.0f); + AssertResult($"ent e{ent.Id} emplace {{ var $name }}", "Foo"); + AssertResult($"player:list emplace {{ var $value }} ", Array.Empty()); + AssertResult($"player:list emplace {{ var $ent }} ", Array.Empty()); + + AssertCompletionSingle($"ent e{ent.Id} emplace ", "{"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ ", "val", "var", "f"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUi", "EntityUi"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid", "EntityUid"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid $", "$value"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ val EntityUid $val", "$value"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ var ", "$value", "$wx", "$paused"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ var $", "$value", "$wx", "$paused"); + AssertCompletionContains($"ent e{ent.Id} emplace {{ var $val", "$value"); + AssertCompletionSingle($"ent e{ent.Id} emplace {{ var $value ", "}"); + + // Intentionally misspelled variable name + AssertCompletionEmpty($"ent e{ent.Id} emplace {{ var $valuie "); + + AssertResult($"ent e{ent.Id} emplace {{ delete $value; i 5 }}", 5 ); + Assert.That(Server.EntMan.Deleted(ent)); + + AssertCompletionContains("i 2 emplace { var ", "$value"); + AssertCompletionInvalid("i 2 emplace { var ", "wx"); + AssertCompletionContains("player:list emplace { var ", "$value", "$ent", "$paused"); + AssertCompletionInvalid("player:list emplace { var ", "wx"); + AssertCompletionContains("i 1 emplace { var $value empla", "emplace"); + AssertCompletionSingle("i 1 emplace { var $value emplace ", "{"); + AssertCompletionContains("player:list emplace { var $ent emplace { var $", "$value", "$wx", "$paused"); + + ParseError("i 1 emplace {{"); + }); + } + + [Test] + public async Task TestCompletions() + { + await Server.WaitAssertion(() => + { + InvokeCommand($"i 1 => $x", out _); + + // Valid/complete commands ending in a whitespace don't generate completions. + AssertCompletionEmpty($"i 1 "); + AssertCompletionEmpty($"i 1 => $x "); + AssertCompletionEmpty($"testvoid "); + + // Without a whitespace, they will still suggest the hint for the command that is currently being typed. + AssertCompletionHint("i 1", "Int32"); + AssertCompletionSingle($"i 1 => $x", "$x"); + AssertCompletionContains($"testvoid", "testvoid"); + + // If an error occurs while parsing something, but tha error is not at the end of the command, we should + // not generate completions. I.e., we don't want to mislead people into thinking a command is valid and is + // expecting additional arguments. + AssertCompletionHint("i a", "Int32"); + AssertCompletionEmpty("i a "); + AssertCompletionEmpty("i a 1"); + AssertCompletionSingle("i $", "$x"); + AssertCompletionEmpty("i $a "); + AssertCompletionSingle("var $", "$x"); + AssertCompletionEmpty("var $a "); + + // Test variable completion + AssertCompletionSingle($"i 1 + $", "$x"); + AssertCompletionSingle($"i 1 + $x", "$x"); + AssertCompletionEmpty($"i 1 + $x "); + + // Completion suggestions are restricted based on the piped type. + AssertCompletionContains("", "i"); + AssertCompletionInvalid("", "testpipedint"); + AssertCompletionEmpty("i 5 "); + AssertCompletionContains("i 5 t", "testpipedint"); + AssertCompletionInvalid("i 5 ", "i"); + AssertCompletionInvalid("i 5; t", "testpipedint"); + AssertCompletionEmpty("i 5; "); + AssertCompletionContains("i 5; i", "i"); + + // Check completions when typing out; var $x + AssertCompletionContains($"va", "val", "var", "vars"); + AssertCompletionContains($"var", "var", "vars"); + AssertCompletionSingle($"var ", "$x"); + AssertCompletionSingle($"var $", "$x"); + AssertCompletionSingle($"var $x", "$x"); + AssertCompletionEmpty($"var $x "); + + // Check completions when typing out: testintstrarg 1 "a" + AssertCompletionContains("testintstrarg", "testintstrarg"); + AssertCompletionHint("testintstrarg ", "Int32"); + AssertCompletionHint("testintstrarg 1", "Int32"); + AssertCompletionSingle("testintstrarg 1 ", "\""); + AssertCompletionHint("testintstrarg 1 \"", ""); + AssertCompletionHint("testintstrarg 1 \"a\"", ""); + AssertCompletionEmpty("testintstrarg 1 \"a\" "); + AssertCompletionHint("testintstrarg 1 \"a\" + ", "Int32"); + + AssertCompletionContains("i 5 iota reduce { ma", "max"); + AssertCompletionContains("i 5 iota reduce { max $", "$x", "$value"); + + AssertCompletionEmpty("notarealcommand "); + AssertCompletionEmpty("i 2 + { notarealcommand "); + }); + } + + [Test] + public async Task TestCustomParser() + { + await Server.WaitAssertion(() => + { + InvokeCommand("i -1 => $x", out _); + + // custom parsers still result in argument expectation erroprs + ParseError("testcustomparser"); + ParseError("testcustomparser "); + + // parser overrides integer parsing. Completion parser falls back to normal integer parsing + AssertResult("testcustomvarrefparser 20", 1); + AssertResult("testcustomparser 20", 1); + + // Custom parser is used to generate completion options + AssertCompletionContains("testcustomvarrefparser", "testcustomparser"); + AssertCompletion("testcustomvarrefparser ", new([new("A")], "B")); + AssertCompletion("testcustomvarrefparser 2", new([new("A")], "B")); + AssertCompletionEmpty("testcustomvarrefparser 2 "); + AssertCompletionContains("testcustomparser", "testcustomparser"); + AssertCompletion("testcustomparser ", new([new("A")], "B")); + AssertCompletion("testcustomparser 2", new([new("A")], "B")); + AssertCompletionEmpty("testcustomparser 2 "); + + // custom Parsers still support variables and blocks, unless explicitly forbidden + AssertResult("testcustomvarrefparser { i 20 }", 20); + AssertResult("testcustomvarrefparser $x", -1); + + ParseError("testcustomparser { i 20 }"); + ParseError("testcustomparser $x"); + + // Variable and block completions still work + AssertCompletionSingle("testcustomvarrefparser $", "$x"); + AssertCompletionSingle("testcustomvarrefparser $x", "$x"); + + AssertCompletionSingle("testcustomvarrefparser { i 2 ", "}"); + AssertCompletionSingle("testcustomvarrefparser { i 2 ", "}"); + }); + } +} diff --git a/Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs b/Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs new file mode 100644 index 000000000..f09a0f257 --- /dev/null +++ b/Robust.UnitTesting/Shared/Toolshed/ToolshedValidationTest.cs @@ -0,0 +1,206 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; +using Robust.Shared.Reflection; +using Robust.Shared.Toolshed; +using Robust.Shared.Toolshed.TypeParsers; +using Robust.Shared.Utility; + +namespace Robust.UnitTesting.Shared.Toolshed; + +public sealed class ToolshedValidationTest : ToolshedTest +{ +#if DEBUG + [Test] + public void TestMethodValidation() + { + // All if these commands are invalid for some reason, and should trip debug asserts + // Theres probably a better way to do this, but these need to have IReflectionManager discoverability disabled + Type[] types = + [ + typeof(TestInvalid1Command), + typeof(TestInvalid2Command), + typeof(TestInvalid3Command), + typeof(TestInvalid4Command), + typeof(TestInvalid5Command), + typeof(TestInvalid6Command), + typeof(TestInvalid7Command), + typeof(TestInvalid8Command), + typeof(TestInvalid9Command), + typeof(TestInvalid10Command), + typeof(TestInvalid11Command), + typeof(TestInvalid12Command), + typeof(TestInvalid13Command), + typeof(TestInvalid14Command), + typeof(TestInvalid15Command), + typeof(TestInvalid16Command), + typeof(TestInvalid17Command), + typeof(TestInvalid18Command) + ]; + + Assert.Multiple(() => + { + foreach (var type in types) + { + var instance = (ToolshedCommand)Activator.CreateInstance(type)!; + Assert.Throws(instance.Init, $"{type.PrettyName()} did not throw a {nameof(InvalidCommandImplementation)} exception"); + } + }); + } +#endif +} + + +#region InvalidCommands +// Not enough type argument parsers +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid1Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation] public void Impl() {} +} + +// too many type argument parsers +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid2Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; + [CommandImplementation] + public void Impl() {} +} + +// The generic has to be the LAST entry, not the first +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid3Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl([PipedArgument] T1 i) {} +} + +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid4Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl([PipedArgument] IEnumerable i) {} +} + +// [TakesPipedTypeAsGeneric] without a [PipedArgument] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid5Command : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl() {} +} + +// Duplicate [PipedArgument] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid6Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([PipedArgument] int arg1, [PipedArgument] int arg2) {} +} + +// Conflicting arguments +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid7Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandArgument, PipedArgument] int arg1) {} +} + +// Conflicting arguments +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid8Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInvocationContext, PipedArgument] int arg1) {} +} + +// wrong [CommandInvocationContext] type +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid9Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInvocationContext] int arg1) {} +} + +// wrong [CommandInverted] type +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid10Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInverted] int arg1) {} +} + +// duplicate [CommandInverted] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid11Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInverted] bool arg1, [CommandInverted] bool arg2) {} +} + +// duplicate [CommandInvocationContext] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid12Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInvocationContext] IInvocationContext arg1, [CommandInvocationContext] IInvocationContext arg2) {} +} + +// Too few type parsers, along with a TakesPipedTypeAsGeneric +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid13Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl([PipedArgument] T3 i) {} +} + +// Too many type parsers, along with a TakesPipedTypeAsGeneric +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid14Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser), typeof(TypeTypeParser)]; + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl([PipedArgument] T2 i) {} +} + +// [TakesPipedTypeAsGeneric] on a non-generic metod +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid15Command : ToolshedCommand +{ + [CommandImplementation, TakesPipedTypeAsGeneric] + public void Impl([PipedArgument] int i) {} +} + +// type arguments on a non-generic metod +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid16Command : ToolshedCommand +{ + public override Type[] TypeParameterParsers => [typeof(TypeTypeParser)]; + [CommandImplementation] public void Impl() {} +} + +// Duplicate mixed explicit/implicit [CommandInvocationContext] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid17Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl([CommandInvocationContext] IInvocationContext arg1, IInvocationContext arg2) {} +} + + +// Duplicate implicit [CommandInvocationContext] +[ToolshedCommand, Reflect(false)] +public sealed class TestInvalid18Command : ToolshedCommand +{ + [CommandImplementation] + public void Impl(IInvocationContext arg1, IInvocationContext arg2) {} +} + +#endregion + +