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
This commit is contained in:
Leon Friedrich
2024-12-21 17:49:11 +11:00
committed by GitHub
parent 6247be2c84
commit 9af119f57a
134 changed files with 5129 additions and 3027 deletions

View File

@@ -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

View File

@@ -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 =

View File

@@ -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<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Length);
var toolshedCommands = _toolshed.DefaultEnvironment.AllCommands();
message.Commands = new List<MsgConCmdReg.Command>(AvailableCommands.Count + toolshedCommands.Count);
var commands = new HashSet<string>();
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<CompletionOption> options = result?.Options ?? Array.Empty<CompletionOption>();
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);
}

View File

@@ -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));
}

View File

@@ -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
)
{

View File

@@ -282,11 +282,31 @@ namespace Robust.Shared.Scripting
return Array.Empty<IConError>();
}
public bool HasErrors => false;
public void ClearErrors()
{
}
public Dictionary<string, object?> Variables { get; } = new();
/// <inheritdoc />
public object? ReadVar(string name)
{
return Variables.GetValueOrDefault(name);
}
/// <inheritdoc />
public void WriteVar(string name, object? value)
{
Variables[name] = value;
}
/// <inheritdoc />
public IEnumerable<string> GetVars()
{
return Variables.Keys;
}
public Dictionary<string, object?> Variables { get; } = new();
private static MemberInfo? ReflectionGetInstanceMember(Type type, MemberTypes memberType, string name)
{

View File

@@ -17,7 +17,7 @@ public sealed record CompletionResult(CompletionOption[] Options, string? Hint)
/// <summary>
/// Type hint string for the current argument being typed.
/// </summary>
public string? Hint { get; init; } = Hint;
public string? Hint { get; set; } = Hint;
public static readonly CompletionResult Empty = new(Array.Empty<CompletionOption>(), null);

View File

@@ -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<T> : IFluentEntityUid
public record struct Entity<T> : IFluentEntityUid, IAsType<EntityUid>
where T : IComponent?
{
public EntityUid Owner;
@@ -44,10 +45,12 @@ public record struct Entity<T> : IFluentEntityUid
comp = Comp;
}
public EntityUid AsType() => Owner;
public override int GetHashCode() => Owner.GetHashCode();
}
public record struct Entity<T1, T2> : IFluentEntityUid
public record struct Entity<T1, T2> : IFluentEntityUid, IAsType<EntityUid>
where T1 : IComponent? where T2 : IComponent?
{
public EntityUid Owner;
@@ -111,9 +114,11 @@ public record struct Entity<T1, T2> : IFluentEntityUid
{
return new Entity<T1>(ent.Owner, ent.Comp1);
}
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3> : IFluentEntityUid
public record struct Entity<T1, T2, T3> : IFluentEntityUid, IAsType<EntityUid>
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent?
{
public EntityUid Owner;
@@ -213,9 +218,11 @@ public record struct Entity<T1, T2, T3> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid
public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid, IAsType<EntityUid>
where T1 : IComponent? where T2 : IComponent? where T3 : IComponent? where T4 : IComponent?
{
public EntityUid Owner;
@@ -339,9 +346,11 @@ public record struct Entity<T1, T2, T3, T4> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid
public record struct Entity<T1, T2, T3, T4, T5> : IFluentEntityUid, IAsType<EntityUid>
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<T1, T2, T3, T4, T5> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid
public record struct Entity<T1, T2, T3, T4, T5, T6> : IFluentEntityUid, IAsType<EntityUid>
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<T1, T2, T3, T4, T5, T6> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid
public record struct Entity<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid, IAsType<EntityUid>
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<T1, T2, T3, T4, T5, T6, T7> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}
public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid
public record struct Entity<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid, IAsType<EntityUid>
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<T1, T2, T3, T4, T5, T6, T7, T8> : IFluentEntityUid
}
#endregion
public EntityUid AsType() => Owner;
}

View File

@@ -106,6 +106,7 @@ namespace Robust.Shared.GameObjects
/// <inheritdoc />
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<T> 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<T>[] AllEntities<T>() where T : IComponent
{
var query = AllEntityQueryEnumerator<T>();
var comps = new Entity<T>[Count<T>()];
var i = 0;
while (query.MoveNext(out var uid, out var comp))
{
comps[i++] = (uid, comp);
}
// Count<T> 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<IComponent>[] AllEntities(Type tComp)
{
var query = AllEntityQueryEnumerator(tComp);
var comps = new Entity<IComponent>[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<T>() where T : IComponent
{
var query = AllEntityQueryEnumerator<T>();
var comps = new EntityUid[Count<T>()];
var i = 0;
while (query.MoveNext(out var uid, out _))
{
comps[i++] = uid;
}
// Count<T> 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<IComponent> AllEntityQueryEnumerator(Type comp)
{
DebugTools.Assert(comp.IsAssignableTo(typeof(IComponent)));
var trait = _entTraitArray[_componentFactory.GetIndex(comp).Value];
return new AllEntityQueryEnumerator<IComponent>(trait);
}
public AllEntityQueryEnumerator<TComp1> AllEntityQueryEnumerator<TComp1>()
where TComp1 : IComponent
{

View File

@@ -47,16 +47,14 @@ namespace Robust.Shared.GameObjects
public static bool TryParse(ReadOnlySpan<char> 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;
}
/// <summary>

View File

@@ -410,6 +410,30 @@ namespace Robust.Shared.GameObjects
/// </summary>
(EntityUid Uid, T Component)[] AllComponents<T>() where T : IComponent;
/// <summary>
/// Returns an array of all entities that have the given component.
/// Use sparingly.
/// </summary>
Entity<T>[] AllEntities<T>() where T : IComponent;
/// <summary>
/// Returns an array of all entities that have the given component.
/// Use sparingly.
/// </summary>
Entity<IComponent>[] AllEntities(Type tComp);
/// <summary>
/// Returns an array uids of all entities that have the given component.
/// Use sparingly.
/// </summary>
EntityUid[] AllEntityUids<T>() where T : IComponent;
/// <summary>
/// Returns an array uids of all entities that have the given component.
/// Use sparingly.
/// </summary>
EntityUid[] AllEntityUids(Type tComp);
/// <summary>
/// Returns all instances of a component in a List.
/// Use sparingly.
@@ -426,6 +450,8 @@ namespace Robust.Shared.GameObjects
/// </summary>
public CompRegistryEntityEnumerator CompRegistryQueryEnumerator(ComponentRegistry registry);
AllEntityQueryEnumerator<IComponent> AllEntityQueryEnumerator(Type comp);
AllEntityQueryEnumerator<TComp1> AllEntityQueryEnumerator<TComp1>()
where TComp1 : IComponent;

View File

@@ -51,28 +51,42 @@ public readonly struct NetEntity : IEquatable<NetEntity>, IComparable<NetEntity>
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<char> 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;
}
/// <summary>

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;
/// </remarks>
/// <remarks><seealso cref="ProtoId{T}"/> for a wrapper of other prototype kinds.</remarks>
[Serializable, NetSerializable]
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>
public readonly record struct EntProtoId(string Id) : IEquatable<string>, IComparable<EntProtoId>, IAsType<string>
{
public static implicit operator string(EntProtoId protoId)
{
@@ -48,6 +49,8 @@ public readonly record struct EntProtoId(string Id) : IEquatable<string>, ICompa
return string.Compare(Id, other.Id, StringComparison.Ordinal);
}
public string AsType() => Id;
public override string ToString() => Id ?? string.Empty;
}

View File

@@ -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;
/// <remarks><seealso cref="EntProtoId"/> for an <see cref="EntityPrototype"/> alias.</remarks>
[Serializable]
[PreferOtherType(typeof(EntityPrototype), typeof(EntProtoId))]
public readonly record struct ProtoId<T>(string Id) : IEquatable<string>, IComparable<ProtoId<T>> where T : class, IPrototype
public readonly record struct ProtoId<T>(string Id) :
IEquatable<string>,
IComparable<ProtoId<T>>,
IAsType<string>
where T : class, IPrototype
{
public static implicit operator string(ProtoId<T> protoId)
{
@@ -46,5 +51,7 @@ public readonly record struct ProtoId<T>(string Id) : IEquatable<string>, ICompa
return string.Compare(Id, other.Id, StringComparison.Ordinal);
}
public string AsType() => Id;
public override string ToString() => Id ?? string.Empty;
}

View File

@@ -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
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class PipedArgumentAttribute : Attribute
{
}
public sealed class PipedArgumentAttribute : Attribute;
/// <summary>
/// Marks an argument in a function as being an argument of a <see cref="ToolshedCommand"/>.
/// This will make it so the argument will get parsed.
/// Marks an argument in a function as being an argument of a <see cref="ToolshedCommand"/>. 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 <see cref="IInvocationContext"/>.
/// </summary>
[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; }
}
/// <summary>
@@ -52,19 +64,17 @@ public sealed class CommandArgumentAttribute : Attribute
/// </summary>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class CommandInvertedAttribute : Attribute
{
}
public sealed class CommandInvertedAttribute : Attribute;
/// <summary>
/// Marks an argument in a function as being where the invocation context should be provided in a <see cref="ToolshedCommand"/>.
/// Marks an argument in a function as being where the invocation context should be provided in a
/// <see cref="ToolshedCommand"/>. This attribute is implicitly present if one of the arguments is of type
/// <see cref="IInvocationContext"/> and has no other relevant attributes.
/// </summary>
/// <seealso cref="IInvocationContext"/>
[AttributeUsage(AttributeTargets.Parameter)]
[MeansImplicitUse]
public sealed class CommandInvocationContextAttribute : Attribute
{
}
public sealed class CommandInvocationContextAttribute : Attribute;
/// <summary>
/// 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 <see cref="ReflectionExtensions.IntersectWithGeneric"/>. It's not very precise.
/// </remarks>
[AttributeUsage(AttributeTargets.Method)]
public sealed class TakesPipedTypeAsGenericAttribute : Attribute
{
}
// Internal because this is just a hack at the moment and should be replaced with proper inference later!
// Overrides type argument parsing to parse a block and then use it's return type as the sole type argument.
internal sealed class MapLikeCommandAttribute : Attribute
{
public bool TakesPipedType;
public MapLikeCommandAttribute(bool takesPipedType = true)
{
TakesPipedType = takesPipedType;
}
}
public sealed class TakesPipedTypeAsGenericAttribute : Attribute;

View File

@@ -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<T> CompEnumerable<T>([PipedArgument] IEnumerable<EntityUid> input)

View File

@@ -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);
}
}

View File

@@ -13,9 +13,9 @@ internal sealed class DoCommand : ToolshedCommand
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Do<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] string command)
string command)
{
if (ctx is not OldShellInvocationContext { } reqCtx || reqCtx.Shell == null)
{

View File

@@ -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<EntityUid> 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<EntityUid>
{
public IEntityManager EntMan { get; } = entMan;
// We create an array as chained commands might modify it.
public EntityUid[]? _arr;
public IEnumerator<EntityUid> GetEnumerator()
{
_arr ??= EntMan.GetEntities().ToArray();
return ((IEnumerable<EntityUid>)_arr).GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
_arr ??= EntMan.GetEntities().ToArray();
return _arr.GetEnumerator();
}
}
}

View File

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

View File

@@ -15,11 +15,7 @@ internal sealed class NearbyCommand : ToolshedCommand
private EntityLookupSystem? _lookup;
[CommandImplementation]
public IEnumerable<EntityUid> Nearby(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] float range
)
public IEnumerable<EntityUid> Nearby([PipedArgument] IEnumerable<EntityUid> input, float range)
{
var rangeLimit = _cfg.GetCVar(CVars.ToolshedNearbyLimit);
if (range > rangeLimit)

View File

@@ -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<EntityUid> Prototyped(
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] Prototype<EntityPrototype> 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);
}

View File

@@ -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<EntityUid> Replace(
[PipedArgument] IEnumerable<EntityUid> input,
[CommandArgument] Prototype<EntityPrototype> prototype
)
public IEnumerable<EntityUid> Replace([PipedArgument] IEnumerable<EntityUid> 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;
}

View File

@@ -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<string, Prototype<EntityPrototype>> proto
)
public EntityUid SpawnAt([PipedArgument] EntityCoordinates target, EntProtoId proto)
{
return Spawn(proto.Evaluate(ctx), target);
return Spawn(proto, target);
}
[CommandImplementation("at")]
public IEnumerable<EntityUid> SpawnAt(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityCoordinates> target,
[CommandArgument] ValueRef<string, Prototype<EntityPrototype>> proto
)
=> target.Select(x => SpawnAt(ctx, x, proto));
public IEnumerable<EntityUid> SpawnAt([PipedArgument] IEnumerable<EntityCoordinates> 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<string, Prototype<EntityPrototype>> 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<EntityUid> SpawnOn(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> target,
[CommandArgument] ValueRef<string, Prototype<EntityPrototype>> proto
)
=> target.Select(x => SpawnOn(ctx, x, proto));
public IEnumerable<EntityUid> SpawnOn([PipedArgument] IEnumerable<EntityUid> 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<string, Prototype<EntityPrototype>> 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<EntityUid> SpawnIn(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> target,
[CommandArgument] ValueRef<string, Prototype<EntityPrototype>> proto
)
=> target.Select(x => SpawnIn(ctx, x, proto));
public IEnumerable<EntityUid> SpawnIn([PipedArgument] IEnumerable<EntityUid> target, EntProtoId proto)
=> target.Select(x => SpawnIn(x, proto));
#endregion
}

View File

@@ -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<EntityUid> With(
[PipedArgument] IEnumerable<EntityUid> 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<EntityPrototype> With(
[PipedArgument] IEnumerable<EntityPrototype> 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<ProtoId<T>> With<T>(
[PipedArgument] IEnumerable<ProtoId<T>> input,
[CommandArgument] ProtoId<T> protoId,
ProtoId<T> protoId,
[CommandInverted] bool inverted
) where T : class, IPrototype
{

View File

@@ -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;

View File

@@ -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<EntityCoordinates> target
)
public EntityUid TpCoords([PipedArgument] EntityUid teleporter, EntityCoordinates target)
{
_xform ??= GetSys<SharedTransformSystem>();
_xform.SetCoordinates(teleporter, target.Evaluate(ctx));
_xform.SetCoordinates(teleporter, target);
return teleporter;
}
[CommandImplementation("coords")]
public IEnumerable<EntityUid> TpCoords(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> teleporters,
[CommandArgument] ValueRef<EntityCoordinates> target
)
=> teleporters.Select(x => TpCoords(ctx, x, target));
public IEnumerable<EntityUid> TpCoords([PipedArgument] IEnumerable<EntityUid> teleporters, EntityCoordinates target)
=> teleporters.Select(x => TpCoords(x, target));
[CommandImplementation("to")]
public EntityUid TpTo(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid teleporter,
[CommandArgument] ValueRef<EntityUid> target
)
public EntityUid TpTo([PipedArgument] EntityUid teleporter, EntityUid target)
{
_xform ??= GetSys<SharedTransformSystem>();
_xform.SetCoordinates(teleporter, Transform(target.Evaluate(ctx)).Coordinates);
_xform.SetCoordinates(teleporter, Transform(target).Coordinates);
return teleporter;
}
[CommandImplementation("to")]
public IEnumerable<EntityUid> TpTo(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> teleporters,
[CommandArgument] ValueRef<EntityUid> target
)
=> teleporters.Select(x => TpTo(ctx, x, target));
public IEnumerable<EntityUid> TpTo([PipedArgument] IEnumerable<EntityUid> teleporters, EntityUid target)
=> teleporters.Select(x => TpTo(x, target));
[CommandImplementation("into")]
public EntityUid TpInto(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] EntityUid teleporter,
[CommandArgument] ValueRef<EntityUid> target
)
public EntityUid TpInto([PipedArgument] EntityUid teleporter, EntityUid target)
{
_xform ??= GetSys<SharedTransformSystem>();
_xform.SetCoordinates(teleporter, new EntityCoordinates(target.Evaluate(ctx), Vector2.Zero));
_xform.SetCoordinates(teleporter, new EntityCoordinates(target, Vector2.Zero));
return teleporter;
}
[CommandImplementation("into")]
public IEnumerable<EntityUid> TpInto(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<EntityUid> teleporters,
[CommandArgument] ValueRef<EntityUid> target
)
=> teleporters.Select(x => TpInto(ctx, x, target));
public IEnumerable<EntityUid> TpInto([PipedArgument] IEnumerable<EntityUid> teleporters, EntityUid target)
=> teleporters.Select(x => TpInto(x, target));
}

View File

@@ -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;
/// <summary>
/// Uses a typecast to convert a type. It does not handle implicit casts, nor explicit ones.

View File

@@ -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<T>([PipedArgument] IEnumerable<T> input, T value, [CommandInverted] bool inverted)
{
return inverted ^ input.Contains(value);
}
}

View File

@@ -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<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] TIn value,
[CommandArgument] Block<TOut> block
[CommandArgument(typeof(EmplaceBlockParser))] Block block
)
{
var emplaceCtx = new EmplaceContext<TIn>(ctx, value, EntityManager);
return block.Invoke(null, emplaceCtx)!;
var emplaceCtx = new EmplaceContext<TIn>(ctx, EntityManager);
emplaceCtx.Value = value;
return (TOut) (block.Invoke(null, emplaceCtx)!);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
IEnumerable<TOut> Emplace<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<TIn> value,
[CommandArgument] Block<TOut> block
[CommandArgument(typeof(EmplaceBlockParser))] Block block
)
{
var emplaceCtx = new EmplaceContext<TIn>(ctx, EntityManager);
foreach (var v in value)
{
var emplaceCtx = new EmplaceContext<TIn>(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<T>(IInvocationContext Inner, T Value, IEntityManager EntityManager) : IInvocationContext
{
public bool CheckInvokable(CommandSpec command, out IConError? error)
private record EmplaceContext<T> : 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<IConError> GetErrors()
{
return Inner.GetErrors();
}
public void ClearErrors()
{
Inner.ClearErrors();
}
public Dictionary<string, object?> Variables => default!; // we never use this.
public IEnumerable<string> GetVars()
{
// note: this lies.
return Inner.GetVars();
}
public object? ReadVar(string name)
{
if (name == "value")
return Value;
if (Value is IEmplaceBreakout breakout)
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<TransformComponent>(id);
var sys = EntityManager.System<SharedTransformSystem>();
var coords = sys.GetWorldPosition(xform);
if (name == "wx")
return coords.X;
else
return coords.Y;
}
case "proto":
case "desc":
case "name":
case "paused":
{
var meta = EntityManager.GetComponent<MetaDataComponent>(id);
switch (name)
{
case "proto":
return meta.EntityPrototype?.ID ?? "";
case "desc":
return meta.EntityDescription;
case "name":
return meta.EntityName;
case "paused":
return meta.EntityPaused;
}
throw new UnreachableException();
}
_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<string> _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<IConError> GetErrors()
{
return _inner.GetErrors();
}
public bool HasErrors => _inner.HasErrors;
public void ClearErrors()
{
_inner.ClearErrors();
}
public IEnumerable<string> 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<SharedTransformSystem>().GetWorldPosition(uid).X,
"wy" => _entMan.System<SharedTransformSystem>().GetWorldPosition(uid).Y,
"proto" => _entMan.GetComponent<MetaDataComponent>(uid).EntityPrototype?.ID ?? "",
"desc" => _entMan.GetComponent<MetaDataComponent>(uid).EntityDescription,
"name" => _entMan.GetComponent<MetaDataComponent>(uid).EntityName,
"paused" => _entMan.GetComponent<MetaDataComponent>(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);
}
/// <summary>
/// Custom block parser for the <see cref="EmplaceCommand"/> is aware of the variables defined within the
/// <see cref="EmplaceContext{T}"/>.
/// </summary>
private sealed class EmplaceBlockParser : CustomTypeParser<Block>
{
public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun? result)
{
// If the piped type is IEnumerable<T> 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;
}
}
/// <summary>
/// This custom parser is for parsing the type returned by the block used in the an <see cref="EmplaceCommand"/>.
/// </summary>
private sealed class EmplaceBlockOutputParser : CustomTypeParser<Type>
{
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<string> VarsOverriden { get; }
public bool TryReadVar(string name, out object? value);
}

View File

@@ -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<T>? Iterate<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] T value,
[CommandArgument] Block<T, T> block,
[CommandArgument] ValueRef<int> times
Block<T, T> 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;
}

View File

@@ -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<T> Repeat<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T value,
[CommandArgument] ValueRef<int> amount
)
=> Enumerable.Repeat(value, amount.Evaluate(ctx));
public IEnumerable<T> Repeat<T>([PipedArgument] T value, int amount)
=> Enumerable.Repeat(value, amount);
}

View File

@@ -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<T> To<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T start,
[CommandArgument] ValueRef<T> end
)
where T : INumber<T>
=> Enumerable.Range(int.CreateTruncating(start), 1 + int.CreateTruncating(end.Evaluate(ctx)! - start)).Select(T.CreateTruncating);
public IEnumerable<T> To<T>([PipedArgument] T start, T end) where T : INumber<T>
=> Enumerable.Range(int.CreateTruncating(start), 1 + int.CreateTruncating(end - start)).Select(T.CreateTruncating);
}

View File

@@ -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<TOut>? Map<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
public IEnumerable<TOut> Map<TOut, TIn>(
IInvocationContext ctx,
[PipedArgument] IEnumerable<TIn> value,
[CommandArgument] Block<TIn, TOut> block
Block<TIn, TOut> block
)
{
return value.Select(x => block.Invoke(x, ctx)).Where(x => x != null).Cast<TOut>();
foreach (var x in value)
{
if (block.Invoke(x, ctx) is { } result)
yield return result;
if (ctx.HasErrors)
break;
}
}
}

View File

@@ -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<T> SortBy<TOrd, T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, TOrd> orderer
Block<T, TOrd> orderer
)
where TOrd : IComparable<TOrd>
=> input.OrderBy(x => orderer.Invoke(x, ctx)!);

View File

@@ -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<T> Sort<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input
)
where T : IComparable<T>
public IEnumerable<T> Sort<T>([PipedArgument] IEnumerable<T> input) where T : IComparable<T>
=> input.Order();
}

View File

@@ -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<T> SortBy<TOrd, T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, TOrd> orderer
Block<T, TOrd> orderer
)
where TOrd : IComparable<TOrd>
=> input.OrderByDescending(x => orderer.Invoke(x, ctx)!);

View File

@@ -9,10 +9,6 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Ordering;
public sealed class SortDownCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Sort<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input
)
where T : IComparable<T>
public IEnumerable<T> Sort<T>([PipedArgument] IEnumerable<T> input) where T : IComparable<T>
=> input.OrderDescending();
}

View File

@@ -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<TOrd> SortBy<TOrd, T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, TOrd> orderer
Block<T, TOrd> orderer
)
where TOrd : IComparable<TOrd>
=> input.Select(x => orderer.Invoke(x, ctx)!).Order();

View File

@@ -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<TOrd> SortBy<TOrd, T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, TOrd> orderer
Block<T, TOrd> orderer
)
where TOrd : IComparable<TOrd>
=> input.Select(x => orderer.Invoke(x, ctx)!).OrderDescending();

View File

@@ -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<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, T> reducer
[CommandArgument(typeof(ReduceBlockParser))] Block reducer
)
=> input.Aggregate((x, next) => reducer.Invoke(x, new ReduceContext<T>(ctx, next))!);
}
internal record ReduceContext<T>(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)
/// <summary>
/// Custom block parser for the <see cref="ReduceCommand"/> that is aware of the "$value" variable.
/// </summary>
private sealed class ReduceBlockParser : CustomTypeParser<Block>
{
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<IConError> 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<string, object?> 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;
}
}
}

View File

@@ -12,7 +12,7 @@ public sealed class SelectCommand : ToolshedCommand
[Dependency] private readonly IRobustRandom _random = default!;
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<TR> Select<TR>([PipedArgument] IEnumerable<TR> enumerable, [CommandArgument] Quantity quantity, [CommandInverted] bool inverted)
public IEnumerable<TR> Select<TR>([PipedArgument] IEnumerable<TR> enumerable, Quantity quantity, [CommandInverted] bool inverted)
{
var arr = enumerable.ToArray();
_random.Shuffle(arr);

View File

@@ -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<T> Take<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] ValueRef<int> amount
)
=> input.Take(amount.Evaluate(ctx));
public IEnumerable<T> Take<T>([PipedArgument] IEnumerable<T> input, int amount)
=> input.Take(amount);
}

View File

@@ -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<TOut> Tee<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
public IEnumerable<TIn> Tee<TOut, TIn>(
IInvocationContext ctx,
[PipedArgument] IEnumerable<TIn> value,
[CommandArgument] Block<TIn, TOut> block
Block<TIn, TOut> block
)
{
return value.Select(x =>
foreach (var x in value)
{
block.Invoke(x, ctx);
return x;
}).Where(x => x != null).Cast<TOut>();
if (ctx.HasErrors)
yield break;
yield return x;
}
}
}

View File

@@ -8,25 +8,17 @@ namespace Robust.Shared.Toolshed.Commands.Generic.Variables ;
public sealed class ArrowCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Arrow<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T input,
[CommandArgument] ValueRef<T> @ref
)
public T Arrow<T>(IInvocationContext ctx, [PipedArgument] T input, WriteableVarRef<T> var)
{
@ref.Set(ctx, input);
ctx.WriteVar(var.Inner.VarName, input);
return input;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public List<T> Arrow<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] ValueRef<List<T>> @ref
)
public List<T> Arrow<T>(IInvocationContext ctx, [PipedArgument] IEnumerable<T> input, WriteableVarRef<List<T>> var)
{
var list = input.ToList();
@ref.Set(ctx, list);
ctx.WriteVar(var.Inner.VarName, list);
return list;
}
}

View File

@@ -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));
}

View File

@@ -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<T> Where<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> input,
[CommandArgument] Block<T, bool> check
Block<T, bool> check
)
{
foreach (var i in input)
{
var res = check.Invoke(i, ctx);
if (ctx.GetErrors().Any())
if (ctx.HasErrors)
yield break;
if (res)

View File

@@ -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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IAdditionOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IAdditionOperators<T, T, T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x + yVal;
return x + y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IAdditionOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y)
.Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
[CommandImplementation]
public Vector2 Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] Vector2 x,
[CommandArgument] ValueRef<Vector2> y
)
public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y)
{
var yVal = y.Evaluate(ctx);
return x + yVal;
return x + y;
}
[CommandImplementation]
public IEnumerable<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<IEnumerable<Vector2>> y
)
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> x, IEnumerable<Vector2> y)
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<Vector2>(right));
return Operation(left, right);
});
}
@@ -64,28 +43,16 @@ public sealed class AddCommand : ToolshedCommand
public sealed class AddVecCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, T y)
where T : IAdditionOperators<T, T, T>
{
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<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<Vector2> y
)
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> 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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : ISubtractionOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : ISubtractionOperators<T, T, T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x - yVal;
return x - y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : ISubtractionOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
[CommandImplementation]
public Vector2 Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] Vector2 x,
[CommandArgument] ValueRef<Vector2> y
)
public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y)
{
var yVal = y.Evaluate(ctx);
return x - yVal;
return x - y;
}
[CommandImplementation]
public IEnumerable<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<IEnumerable<Vector2>> y
)
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> x, IEnumerable<Vector2> y)
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<Vector2>(right));
return Operation(left, right);
});
}
@@ -147,28 +93,16 @@ public sealed class SubtractCommand : ToolshedCommand
public sealed class SubVecCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, T y)
where T : ISubtractionOperators<T, T, T>
{
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<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<Vector2> y
)
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> 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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IMultiplyOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IMultiplyOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x * yVal;
return x * y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IMultiplyOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
[CommandImplementation]
public Vector2 Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] Vector2 x,
[CommandArgument] ValueRef<Vector2> y
)
public Vector2 Operation([PipedArgument] Vector2 x, Vector2 y)
{
var yVal = y.Evaluate(ctx);
return x * yVal;
return x * y;
}
[CommandImplementation]
public IEnumerable<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<IEnumerable<Vector2>> y
)
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> x, IEnumerable<Vector2> y)
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<Vector2>(right));
return Operation(left, right);
});
}
@@ -228,28 +143,16 @@ public sealed class MultiplyCommand : ToolshedCommand
public sealed class MulVecCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, T y)
where T : IMultiplyOperators<T, T, T>
{
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<Vector2> Operation(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<Vector2> x,
[CommandArgument] ValueRef<Vector2> y
)
public IEnumerable<Vector2> Operation([PipedArgument] IEnumerable<Vector2> 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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumberBase<T>
public T Operation<T>([PipedArgument] T x, T y) where T : INumberBase<T>
{
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<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
where T : INumberBase<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y) where T : INumberBase<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -292,21 +181,12 @@ public sealed class DivideCommand : ToolshedCommand
public sealed class DivVecCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
)
where T : INumberBase<T>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, T y) where T : INumberBase<T>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IModulusOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IModulusOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x % yVal;
return x % y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IModulusOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -343,17 +213,9 @@ public sealed class ModulusCommand : ToolshedCommand
public sealed class ModVecCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
)
where T : IModulusOperators<T, T, T>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, T y) where T : IModulusOperators<T, T, T>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumberBase<T>
public T Operation<T>([PipedArgument] T x, T y) where T : INumberBase<T>
{
var yVal = y.Evaluate(ctx)!;
return T.MinMagnitude(x, yVal);
return T.MinMagnitude(x, y);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
where T : INumberBase<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y) where T : INumberBase<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -391,28 +242,17 @@ public sealed class MinCommand : ToolshedCommand
public sealed class MaxCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumberBase<T>
public T Operation<T>([PipedArgument] T x, T y) where T : INumberBase<T>
{
var yVal = y.Evaluate(ctx)!;
return T.MaxMagnitude(x, yVal);
return T.MaxMagnitude(x, y);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
where T : INumberBase<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y) where T : INumberBase<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -421,28 +261,18 @@ public sealed class MaxCommand : ToolshedCommand
public sealed class BitAndCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x & yVal;
return x & y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -450,28 +280,18 @@ public sealed class BitAndCommand : ToolshedCommand
public sealed class BitAndNotCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x & ~yVal;
return x & ~y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -479,28 +299,18 @@ public sealed class BitAndNotCommand : ToolshedCommand
public sealed class BitOrCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x | yVal;
return x | y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -508,28 +318,18 @@ public sealed class BitOrCommand : ToolshedCommand
public sealed class BitOrNotCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x | ~yVal;
return x | ~y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -537,28 +337,18 @@ public sealed class BitOrNotCommand : ToolshedCommand
public sealed class BitXorCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x ^ yVal;
return x ^ y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -566,28 +356,18 @@ public sealed class BitXorCommand : ToolshedCommand
public sealed class BitXnorCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x, T y) where T : IBitwiseOperators<T, T, T>
{
var yVal = y.Evaluate(ctx)!;
return x ^ ~yVal;
return x ^ ~y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y)
where T : IBitwiseOperators<T, T, T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}
@@ -595,22 +375,14 @@ public sealed class BitXnorCommand : ToolshedCommand
public sealed class BitNotCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x
)
where T : IBitwiseOperators<T, T, T>
public T Operation<T>([PipedArgument] T x) where T : IBitwiseOperators<T, T, T>
{
return ~x;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x
)
where T : IBitwiseOperators<T, T, T>
=> x.Select(v => Operation<T>(ctx, v));
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x) where T : IBitwiseOperators<T, T, T>
=> x.Select(Operation);
}
#endregion
@@ -619,15 +391,13 @@ public sealed class BitNotCommand : ToolshedCommand
public sealed class NegCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>([PipedArgument] T x)
where T : IUnaryNegationOperators<T, T>
public T Operation<T>([PipedArgument] T x) where T : IUnaryNegationOperators<T, T>
{
return -x;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x)
where T : IUnaryNegationOperators<T, T>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x) where T : IUnaryNegationOperators<T, T>
=> x.Select(Operation);
}
@@ -635,14 +405,12 @@ public sealed class NegCommand : ToolshedCommand
public sealed class AbsCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>([PipedArgument] T x)
where T : INumberBase<T>
public T Operation<T>([PipedArgument] T x) where T : INumberBase<T>
{
return T.Abs(x);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x)
where T : INumberBase<T>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x) where T : INumberBase<T>
=> x.Select(Operation);
}

View File

@@ -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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : INumber<T>
public bool Comparison<T>([PipedArgument] T x, T y) where T : INumber<T>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
public bool Comparison<T>([PipedArgument] T x, T y)
where T : IComparisonOperators<T, T, bool>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
public bool Comparison<T>([PipedArgument] T x, T y)
where T : INumber<T>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
public bool Comparison<T>([PipedArgument] T x, T y)
where T : IComparisonOperators<T, T, bool>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
public bool Comparison<T>([PipedArgument] T x, T y)
where T : IEquatable<T>
{
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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
public bool Comparison<T>([PipedArgument] T x, T y)
where T : IEquatable<T>
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return false;
return !x.Equals(yVal);
return !x.Equals(y);
}
}

View File

@@ -160,14 +160,14 @@ public sealed class TruncCommand : ToolshedCommand
public sealed class Round2FracCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>([PipedArgument] T x, [CommandArgument] int frac)
public T Operation<T>([PipedArgument] T x, int frac)
where T : IFloatingPoint<T>
{
return T.Round(x, frac);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, [CommandArgument] int frac)
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, int frac)
where T : IFloatingPoint<T>
=> 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<T>([PipedArgument] T x)
where T : IFloatingPoint<T>
public int Operation<T>([PipedArgument] T x) where T : IFloatingPoint<T>
{
return x.GetExponentShortestBitLength();
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<int> Operation<T>([PipedArgument] IEnumerable<T> x)
where T : IFloatingPoint<T>
public IEnumerable<int> Operation<T>([PipedArgument] IEnumerable<T> x) where T : IFloatingPoint<T>
=> x.Select(Operation);
}
@@ -242,44 +240,24 @@ public sealed class ExponentShortestBitCountCommand : ToolshedCommand
public sealed class StepNextCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x
)
where T : IFloatingPointIeee754<T>
{
return T.BitIncrement(x);
}
public T Operation<T>([PipedArgument] T x) where T : IFloatingPointIeee754<T>
=> T.BitIncrement(x);
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x
)
where T : IFloatingPointIeee754<T>
=> x.Select(v => Operation(ctx, v));
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x) where T : IFloatingPointIeee754<T>
=> x.Select(Operation);
}
[ToolshedCommand]
public sealed class StepPrevCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x
)
where T : IFloatingPointIeee754<T>
{
return T.BitDecrement(x);
}
public T Operation<T>([PipedArgument] T x) where T : IFloatingPointIeee754<T>
=> T.BitDecrement(x);
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x
)
where T : IFloatingPointIeee754<T>
=> x.Select(v => Operation(ctx, v));
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x) where T : IFloatingPointIeee754<T>
=> x.Select(Operation);
}
#endregion

View File

@@ -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<string> y
string y
)
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x + yVal;
return x + y;
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Join<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
IEnumerable<T> 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<T> Append<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<T> y
T y
)
{
var yVal = y.Evaluate(ctx);
if (yVal is null)
return x;
return x.Append(yVal);
return x.Append(y);
}
}

View File

@@ -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<TOut, TIn>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] TIn? value,
[CommandArgument] Block<TIn, TOut> follower
Block<TIn, TOut> follower
)
where TIn : unmanaged
{
@@ -30,9 +36,9 @@ public sealed class OrValueCommand : ToolshedCommand
[CommandImplementation, TakesPipedTypeAsGeneric]
public T OrValue<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] T? value,
[CommandArgument] ValueRef<T> alternate
ValueRef<T> alternate
)
where T : class
{
@@ -41,9 +47,9 @@ public sealed class OrValueCommand : ToolshedCommand
[CommandImplementation, TakesPipedTypeAsGeneric]
public T OrValue<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] T? value,
[CommandArgument] ValueRef<T> alternate
ValueRef<T> alternate
)
where T : unmanaged
{
@@ -58,7 +64,7 @@ public sealed class DebugPrintCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T DebugPrint<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] T value
)
{
@@ -68,7 +74,7 @@ public sealed class DebugPrintCommand : ToolshedCommand
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> DebugPrint<T>(
[CommandInvocationContext] IInvocationContext ctx,
IInvocationContext ctx,
[PipedArgument] IEnumerable<T> value
)
{

View File

@@ -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<TOut, T>([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<TOut, T>([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<TOut, T>([PipedArgument] T x)

View File

@@ -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;
}

View File

@@ -9,29 +9,16 @@ namespace Robust.Shared.Toolshed.Commands.Math;
public sealed class PowCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IPowerFunctions<T>
public T Operation<T>([PipedArgument] T x,T y) where T : IPowerFunctions<T>
{
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<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
where T : IPowerFunctions<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y) where T : IPowerFunctions<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}

View File

@@ -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<int> 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<int> 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<float> 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<float> 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<float> prob
)
=> _random.Prob(prob.Evaluate(ctx));
}

View File

@@ -42,27 +42,20 @@ public sealed class RootCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<int> y
int y
)
where T : IRootFunctions<T>
{
var yVal = y.Evaluate(ctx);
return T.RootN(x, yVal);
return T.RootN(x, y);
}
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<int>> y
)
where T : IRootFunctions<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<int> y) where T : IRootFunctions<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<int>(right));
return Operation(left, right);
});
}
@@ -70,29 +63,16 @@ public sealed class RootCommand : ToolshedCommand
public sealed class HypotCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public T Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] T x,
[CommandArgument] ValueRef<T> y
)
where T : IRootFunctions<T>
public T Operation<T>([PipedArgument] T x, T y) where T : IRootFunctions<T>
{
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<T> Operation<T>(
[CommandInvocationContext] IInvocationContext ctx,
[PipedArgument] IEnumerable<T> x,
[CommandArgument] ValueRef<IEnumerable<T>> y
)
where T : IRootFunctions<T>
=> x.Zip(y.Evaluate(ctx)!).Select(inp =>
public IEnumerable<T> Operation<T>([PipedArgument] IEnumerable<T> x, IEnumerable<T> y) where T : IRootFunctions<T>
=> x.Zip(y).Select(inp =>
{
var (left, right) = inp;
return Operation(ctx, left, new ValueRef<T>(right));
return Operation(left, right);
});
}

View File

@@ -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);

View File

@@ -9,7 +9,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc;
public sealed class CmdCommand : ToolshedCommand
{
[CommandImplementation("list")]
public IEnumerable<CommandSpec> List([CommandInvocationContext] IInvocationContext ctx)
public IEnumerable<CommandSpec> 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

View File

@@ -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());
}
}

View File

@@ -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");
}

View File

@@ -11,7 +11,7 @@ namespace Robust.Shared.Toolshed.Commands.Misc;
public sealed class SearchCommand : ToolshedCommand
{
[CommandImplementation, TakesPipedTypeAsGeneric]
public IEnumerable<FormattedMessage> Search<T>([PipedArgument] IEnumerable<T> input, [CommandArgument] string term)
public IEnumerable<FormattedMessage> Search<T>([PipedArgument] IEnumerable<T> input, string term)
{
var list = input.Select(x => Toolshed.PrettyPrintType(x, out _)).ToList();
return list.Where(x => x.Contains(term, StringComparison.InvariantCultureIgnoreCase)).Select(x =>

View File

@@ -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();

View File

@@ -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<Type> Tree([CommandInvocationContext] IInvocationContext ctx, [PipedArgument] object? input)
public IEnumerable<Type> Tree(IInvocationContext ctx, [PipedArgument] object? input)
{
var t = input is Type ? (Type)input : input!.GetType();
return Toolshed.AllSteppedTypes(t);

View File

@@ -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)
{

View File

@@ -34,7 +34,7 @@ internal sealed class MethodsCommand : ToolshedCommand
}
[CommandImplementation("overridesfrom")]
public IEnumerable<MethodInfo> OverridesFrom([PipedArgument] IEnumerable<Type> types, [CommandArgument] Type t)
public IEnumerable<MethodInfo> OverridesFrom([PipedArgument] IEnumerable<Type> types, Type t)
{
foreach (var ty in types)
{

View File

@@ -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;
}

View File

@@ -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<EntityUid> ent, [CommandInvocationContext] IInvocationContext ctx) => ent.Evaluate(ctx);
public EntityUid Ent(EntityUid uid) => uid;
}

View File

@@ -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<T>(
[CommandInvocationContext] IInvocationContext ctx,
[CommandArgument] ValueRef<T> value
) => value.Evaluate(ctx)!;
public T Val<T>(T value) => value;
}

View File

@@ -0,0 +1,20 @@
using System;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Toolshed.TypeParsers;
namespace Robust.Shared.Toolshed.Commands.Values;
/// <summary>
/// Variant of the <see cref="ValCommand"/> that only works for variable references, and automatically infers the type
/// from the variable's value.
/// </summary>
[ToolshedCommand]
public sealed class VarCommand : ToolshedCommand
{
private static Type[] _parsers = [typeof(VarTypeParser)];
public override Type[] TypeParameterParsers => _parsers;
[CommandImplementation]
public T Var<T>(IInvocationContext ctx, VarRef<T> var)
=> var.Evaluate(ctx)!;
}

View File

@@ -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);

View File

@@ -8,14 +8,14 @@ namespace Robust.Shared.Toolshed.Commands.Vfs;
internal sealed class LsCommand : VfsCommand
{
[CommandImplementation("here")]
public IEnumerable<ResPath> LsHere([CommandInvocationContext] IInvocationContext ctx)
public IEnumerable<ResPath> LsHere(IInvocationContext ctx)
{
var curPath = CurrentPath(ctx);
return Resources.ContentGetDirectoryEntries(curPath).Select(x => curPath/x);
}
[CommandImplementation("in")]
public IEnumerable<ResPath> LsIn([CommandInvocationContext] IInvocationContext ctx, [CommandArgument] ResPath @in)
public IEnumerable<ResPath> LsIn(IInvocationContext ctx, ResPath @in)
{
var curPath = CurrentPath(ctx);
if (@in.IsRooted)

View File

@@ -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.
/// <summary>
/// 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; }
}

View File

@@ -24,11 +24,7 @@ public interface IInvocationContext
/// </remarks>
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
/// </remarks>
public IEnumerable<IConError> GetErrors();
public bool HasErrors { get; }
/// <summary>
/// Clears the list of unobserved errors.
/// </summary>
@@ -123,14 +121,6 @@ public interface IInvocationContext
/// </remarks>
public void ClearErrors();
/// <summary>
/// The backing variable storage.
/// </summary>
/// <remarks>
/// You don't have to use this at all.
/// </remarks>
protected Dictionary<string, object?> Variables { get; }
/// <summary>
/// Reads the given variable from the context.
/// </summary>
@@ -139,11 +129,7 @@ public interface IInvocationContext
/// <remarks>
/// This may behave arbitrarily, but it's advised it behave somewhat sanely.
/// </remarks>
public virtual object? ReadVar(string name)
{
Variables.TryGetValue(name, out var res);
return res;
}
object? ReadVar(string name);
/// <summary>
/// Writes the given variable to the context.
@@ -153,17 +139,24 @@ public interface IInvocationContext
/// <remarks>
/// Writes may be ignored or manipulated.
/// </remarks>
public virtual void WriteVar(string name, object? value)
{
Variables[name] = value;
}
void WriteVar(string name, object? value);
/// <summary>
/// Whether or not a variable is read-only. Used for variable name auto-completion.
/// </summary>
bool IsReadonlyVar(string name) => false;
/// <summary>
/// Provides a list of all variables that have been written to at some point.
/// </summary>
/// <returns>List of all variables.</returns>
public virtual IEnumerable<string> GetVars()
public IEnumerable<string> GetVars();
}
public sealed class ReadonlyVariableError(string name) : ConError
{
public override FormattedMessage DescribeInner()
{
return Variables.Keys;
return FormattedMessage.FromUnformatted($"${name} is a read-only variable.");
}
}

View File

@@ -1,5 +1,4 @@
using JetBrains.Annotations;
using Robust.Shared.Player;
using Robust.Shared.Player;
using Robust.Shared.Toolshed.Errors;
namespace Robust.Shared.Toolshed;

View File

@@ -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
/// </summary>
public IConsoleShell? Shell;
/// <inheritdoc />
public NetUserId? User { get; }
/// <inheritdoc />
public ICommonSession? Session => Shell?.Player;
public OldShellInvocationContext(IConsoleShell shell)
{
IoCManager.InjectDependencies(this);
@@ -30,12 +37,6 @@ internal sealed class OldShellInvocationContext : IInvocationContext
User = Session?.UserId;
}
/// <inheritdoc />
public NetUserId? User { get; }
/// <inheritdoc />
public ICommonSession? Session => Shell?.Player;
/// <inheritdoc />
public void WriteLine(string line)
{
@@ -60,6 +61,8 @@ internal sealed class OldShellInvocationContext : IInvocationContext
return _errors;
}
public bool HasErrors => _errors.Count > 0;
/// <inheritdoc />
public void ClearErrors()
{
@@ -67,6 +70,33 @@ internal sealed class OldShellInvocationContext : IInvocationContext
}
/// <inheritdoc />
public object? ReadVar(string name)
{
if (name == "self" && Session?.AttachedEntity is { } ent)
return ent;
return Variables.GetValueOrDefault(name);
}
/// <inheritdoc />
public void WriteVar(string name, object? value)
{
if (name == "self")
ReportError(new ReadonlyVariableError("self"));
else
Variables[name] = value;
}
/// <inheritdoc />
public bool IsReadonlyVar(string name) => name == "self";
/// <inheritdoc />
public IEnumerable<string> GetVars()
{
return Session?.AttachedEntity != null
? Variables.Keys.Append("self")
: Variables.Keys;
}
public Dictionary<string, object?> Variables { get; } = new();
}

View File

@@ -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;
/// <summary>
/// <see cref="IInvocationContext"/> that wraps some other context and provides some local variables.
/// </summary>
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<IConError> GetErrors() => inner.GetErrors();
public bool HasErrors => inner.HasErrors;
public void ClearErrors() => inner.ClearErrors();
public Dictionary<string, object?> LocalVars = new();
public HashSet<string>? 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<string> GetVars()
{
foreach (var key in LocalVars.Keys)
{
yield return key;
}
foreach (var key in inner.GetVars())
{
if (!LocalVars.ContainsKey(key))
yield return key;
}
}
}

View File

@@ -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<MethodInfo>)> BySubCommand(this IEnumerable<MethodInfo> methods)
{
var output = new Dictionary<string, List<MethodInfo>>();
foreach (var method in methods)
{
var subCommand = method.GetCustomAttribute<CommandImplementationAttribute>()!.SubCommand ?? "";
if (!output.TryGetValue(subCommand, out var methodList))
{
methodList = new();
output[subCommand] = methodList;
}
methodList.Add(method);
}
return output.Select(x => (x.Key, x.Value));
}
public static Type StepDownConstraints(this Type t)
{
if (!t.IsGenericType || t.IsGenericTypeDefinition)
@@ -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<CommandArgumentAttribute>())
return true;
if (param.HasCustomAttribute<CommandInvertedAttribute>())
return false;
if (param.HasCustomAttribute<PipedArgumentAttribute>())
return false;
if (param.HasCustomAttribute<CommandInvocationContextAttribute>())
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<PipedArgumentAttribute>() is not null).ToList();
return p.FirstOrDefault();
return method.GetParameters().SingleOrDefault(x => x.HasCustomAttribute<PipedArgumentAttribute>());
}
public static IEnumerable<ParameterInfo> ConsoleGetArguments(this MethodInfo method)
public static bool ConsoleHasInvertedArgument(this MethodInfo method)
{
return method.GetParameters().Where(x => x.GetCustomAttribute<CommandArgumentAttribute>() is not null);
return method.GetParameters().Any(x => x.HasCustomAttribute<CommandInvertedAttribute>());
}
public static Expression CreateEmptyExpr(this Type t)
@@ -159,6 +190,28 @@ internal static class ReflectionExtensions
throw new NotImplementedException();
}
// IEnumerable<EntityUid> ^ IEnumerable<T> -> EntityUid
public static Type Intersect(this Type left, Type right)
{
if (!left.IsGenericType)
return left;
if (!right.IsGenericType)
return left;
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()}");

View File

@@ -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
/// <summary>
/// A simple block of commands.
/// </summary>
[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} }}";
}
}
/// <summary>
/// Something more akin to actual expressions.
/// A block of commands that take in no input, and return <see cref="T"/>.
/// </summary>
public sealed class Block<T>
[Virtual]
public class Block<T>(CommandRun expr) : Block(expr)
{
internal CommandRun<T> CommandRun { get; set; }
public static bool TryParse(bool doAutoComplete, ParserContext parserContext, Type? pipedType,
[NotNullWhen(true)] out Block<T>? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error)
public static bool TryParse(ParserContext ctx,
[NotNullWhen(true)] out Block<T>? block
)
{
parserContext.ConsumeWhitespace();
var enclosed = parserContext.EatMatch('{');
if (enclosed)
parserContext.PushTerminator("}");
CommandRun<T>.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<T>(expr);
block = new Block<T>(run);
return true;
}
public Block(CommandRun<T> 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<TIn, TOut>
/// <summary>
/// A block of commands that take in <see cref="TIn"/>, and return <see cref="TOut"/>.
/// </summary>
[Virtual]
public class Block<TIn, TOut>(CommandRun expr) : Block(expr)
{
internal CommandRun<TIn, TOut> CommandRun { get; set; }
public static bool TryParse(bool doAutoComplete, ParserContext parserContext, Type? pipedType,
[NotNullWhen(true)] out Block<TIn, TOut>? block, out ValueTask<(CompletionResult?, IConError?)>? autoComplete, out IConError? error)
public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block<TIn, TOut>? block)
{
parserContext.ConsumeWhitespace();
var enclosed = parserContext.EatMatch('{');
if (enclosed)
parserContext.PushTerminator("}");
CommandRun<TIn, TOut>.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<TIn, TOut>(expr);
block = new Block<TIn, TOut>(run);
return true;
}
public Block(CommandRun<TIn, TOut> 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; }
}

View File

@@ -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;
/// <summary>
/// A "run" of commands. Not a true expression.
/// </summary>
public sealed class CommandRun
{
public readonly List<(ParsedCommand, Vector2i)> Commands;
private readonly string _originalExpr;
/// <summary>
/// The original string that contains the substring from which this command run was parsed.
/// </summary>
public readonly string OriginalExpr;
public static bool TryParse(bool doAutocomplete,
ParserContext parserContext,
/// <summary>
/// The list of parsed commands, along with the start and end indices in <see cref="OriginalExpr"/>
/// </summary>
public readonly List<(ParsedCommand, Vector2i)> Commands;
#region Misc Debug Properties
/// <summary>
/// The type returned by the last command in <see cref="Commands"/>
/// </summary>
public readonly Type? ReturnType;
/// <summary>
/// The type that should get piped into the first command in <see cref="Commands"/>
/// </summary>
public readonly Type? PipedType;
/// <summary>
/// The starting index of the first command in <see cref="Commands"/>
/// </summary>
public readonly int StartIndex;
/// <summary>
/// The ending index of the last command in <see cref="Commands"/>
/// </summary>
public readonly int EndIndex;
/// <summary>
/// The substring of <see cref="OriginalExpr"/> from which all of the commands in the run were parsed.
/// </summary>
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);
}
/// <summary>
/// Attempt to parse a sequence of commands that initially take in the given piped type.
/// </summary>
/// <param name="ctx">The parser context</param>
/// <param name="pipedType">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</param>
/// <param name="targetOutput">The desired output type of the final command in the sequence. Null implies no constraint. The <see cref="Void"/> type implies that the final command should not return a value</param>
/// <param name="expr">The expression that was generated</param>
/// <returns></returns>
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<TIn, TOut>
{
internal readonly CommandRun InnerCommandRun;
public static bool TryParse(bool blockMode, bool doAutoComplete, ParserContext parserContext, bool once,
[NotNullWhen(true)] out CommandRun<TIn, TOut>? expr,
out ValueTask<(CompletionResult?, IConError?)>? autocomplete, out IConError? error)
public static bool TryParse(ParserContext ctx, [NotNullWhen(true)] out CommandRun<TIn, TOut>? 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<TIn, TOut>
return (TOut?) res;
}
private CommandRun(CommandRun commandRun)
internal CommandRun(CommandRun commandRun)
{
InnerCommandRun = commandRun;
}
public override string ToString()
{
return InnerCommandRun.ToString();
}
}
public sealed class CommandRun<TRes>
{
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<TRes>? expr, out ValueTask<(CompletionResult?, IConError?)>? completion,
out IConError? error)
public static bool TryParse(
ParserContext ctx,
Type? pipedType,
[NotNullWhen(true)] out CommandRun<TRes>? 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<TRes>
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 (;)");
}
}

View File

@@ -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;
/// <summary>
/// Interface for attempting to infer the type of a variable while parsing toolshed commands.
/// </summary>
/// <remarks>
/// The variable parser being used by the <see cref="ParserContext"/> 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 <see cref="LocalVarParser"/>
/// </remarks>
public interface IVariableParser
{
public static readonly IVariableParser Empty = new EmptyVarParser();
/// <summary>
/// Attempt to get the type of the variable with the given name.
/// </summary>
bool TryParseVar(string name, [NotNullWhen(true)] out Type? type);
/// <summary>
/// Generate completion options containing valid variable names along with their types.
/// </summary>
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, "<variable name>");
}
/// <summary>
/// Generate completion options containing valid variable names along with their types.
/// </summary>
CompletionResult GenerateCompletions<T>(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,$"<Variable of type {typeof(T).PrettyName()}>");
}
/// <summary>
/// Whether or not a variable is read-only. Used for variable name auto-completion.
/// </summary>
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;
}
}
}
/// <summary>
/// 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 <see cref="LocalVarParser"/>.
/// </summary>
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);
}
/// <summary>
/// Simple wrapper around a variable type parser that modifies / overrides the types returned by some other parser.
/// </summary>
public sealed class LocalVarParser(IVariableParser inner) : IVariableParser
{
public readonly IVariableParser Inner = inner;
public Dictionary<string, Type>? Variables;
public HashSet<string>? 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);
}
}

View File

@@ -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<CommandInvocationArguments, object?>;
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<Type>()};
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)
/// <summary>
/// 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 <see cref="CommandArgumentBundle"/>.
/// </summary>
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()), "<command>"), 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()), "<command>"), 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()), "<command>"), 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()), "<command>"), 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<Type>(), 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; }
}

View File

@@ -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);
}

View File

@@ -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;
/// <summary>
/// The parser to use when trying autocomplete variable names or to infer the type of a variable.
/// </summary>
/// <remarks>
/// 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
/// <see cref="VarRef{T}.BadVarTypeError"/> if the command that was parsed relied on knowing a variable's type.
/// </remarks>
public IVariableParser VariableParser;
/// <summary>
/// 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.
/// </summary>
public CommandArgumentBundle Bundle;
/// <summary>
/// Whether or not to generate auto-completion options.
/// </summary>
public bool GenerateCompletions;
/// <summary>
/// Any auto-completion suggestions that have been generated while parsing.
/// </summary>
public CompletionResult? Completions;
/// <summary>
/// Any errors that have come up while parsing. This is generally null while <see cref="GenerateCompletions"/> is true,
/// under the assumption that the command is purely being parsed to gather completion suggestions, not to try evaluate it.
/// </summary>
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;
/// <summary>
/// Whether the parser has reached the end of the input.
/// </summary>
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;
}
/// <remarks>
@@ -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;
}
/// <remarks>
@@ -110,26 +155,24 @@ public sealed partial class ParserContext
/// </remarks>
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<char> buffer = stackalloc char[2];
c.EncodeToUtf16(buffer);
if (c.Utf16SequenceLength > 1)
return '\x01';
return buffer[0];
}
Span<char> 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<string> _terminatorStack = new();
private Stack<Rune> _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<string> TerminatorStack;
public ParserRestorePoint(int index, Stack<string> terminatorStack)
/// <summary>
/// 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.
/// </summary>
public bool CheckInvokable(CommandSpec cmd)
{
Index = index;
TerminatorStack = terminatorStack;
return Toolshed.CheckInvokable(cmd, Session, out Error);
}
/// <summary>
/// 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.
/// </summary>
public bool CheckInvokable<T>() where T : ToolshedCommand
{
if (!Environment.TryGetCommands<T>(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();
}
/// <summary>
/// Attempts to consume a single command terminator, which is either a ';' or a newline (if <see cref="NoMultilineExprs"/> is
/// enabled).
/// </summary>
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'));
}
/// <summary>
/// Attempts to repeatedly consume command terminators, and return true if any were consumed.
/// </summary>
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<Rune> TerminatorStack,
CommandArgumentBundle Bundle,
IVariableParser VariableParser);
public record OutOfInputError : IConError
{
public FormattedMessage DescribeInner()

View File

@@ -6,107 +6,108 @@ using Robust.Shared.Utility;
namespace Robust.Shared.Toolshed.Syntax;
public sealed class ValueRef<T> : ValueRef<T, T>
/// <summary>
/// This class is used represent toolshed command arguments that are either a reference to a Toolshed variable
/// (<see cref="VarRef{T}"/>), a block of commands that need to be evaluated (<see cref="Block{T}"/>), or simply some
/// specific value that has already been parsed/evaluated (<see cref="ParsedValueRef{T}"/>).
/// </summary>
public abstract class ValueRef<T>
{
public ValueRef(ValueRef<T, T> inner)
{
InnerBlock = inner.InnerBlock;
VarName = inner.VarName;
HasValue = inner.HasValue;
Value = inner.Value;
Expression = inner.Expression;
RefSpan = inner.RefSpan;
}
public ValueRef(string varName) : base(varName)
{
}
public abstract T? Evaluate(IInvocationContext ctx);
public ValueRef(Block<T> 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<T> @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<T, TAuto>
[Obsolete("Use EntProtoId / ProtoId<T>")]
public sealed class ValueRef<T, TAuto>(ValueRef<T> inner) : ValueRef<T>
{
internal Block<T>? 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<T>(Block<T> block) : ValueRef<T>
{
public override T? Evaluate(IInvocationContext ctx) => block.Invoke(ctx);
}
/// <summary>
/// This class is used represent toolshed command arguments that references to a Toolshed variable.
/// I.e., something accessible via <see cref="IInvocationContext.ReadVar"/>.
/// </summary>
public sealed class VarRef<T>(string varName) : ValueRef<T>
{
/// <summary>
/// The name of the variable.
/// </summary>
public readonly string VarName = varName;
public override T? Evaluate(IInvocationContext ctx)
{
VarName = varName;
}
public ValueRef(Block<T> innerBlock)
{
InnerBlock = innerBlock;
}
public ValueRef(T value)
{
Value = value;
HasValue = true;
}
public bool LikelyConst => VarName is not null || HasValue;
public T? Evaluate(IInvocationContext ctx)
{
if (Value is not null && HasValue)
{
return Value;
}
else if (VarName is not null)
{
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<T>(VarRef<T> inner) : ValueRef<T>
{
public FormattedMessage DescribeInner()
public readonly VarRef<T> 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);
}
}
/// <summary>
/// This class represents a <see cref="ValueRef{T}"/> command argument that simply corresponds to a specific value of
/// some type that has already been parsed/evaluated.
/// </summary>
internal sealed class ParsedValueRef<T>(T? value) : ValueRef<T>
{
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; }
}

View File

@@ -154,6 +154,10 @@ public abstract partial class ToolshedCommand
where T: EntitySystem
=> EntitySystemManager.GetEntitySystem<T>();
// GetSys is just too many letters to type
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected T Sys<T>() where T: EntitySystem => EntitySystemManager.GetEntitySystem<T>();
/// <summary>
/// A shorthand for retrieving an entity query.
/// </summary>

View File

@@ -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.
/// </summary>
public string Description(string? subCommand)
=> Loc.GetString(UnlocalizedDescription(subCommand));
{
CommandImplementors.TryGetValue(subCommand ?? string.Empty, out var impl);
return impl?.Description() ?? string.Empty;
}
/// <summary>
/// Returns the locale string for a command's description.
/// </summary>
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;
}
/// <summary>
@@ -32,65 +25,12 @@ public abstract partial class ToolshedCommand
/// </summary>
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;
}
/// <inheritdoc/>
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;
}
}

View File

@@ -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<string, ToolshedCommandImplementor> _implementors = new();
private readonly Dictionary<(CommandDiscriminator, string?), List<MethodInfo>> _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;
}
/// <summary>
/// Does its best to find an implementation that can deal with the given types
/// </summary>
internal List<MethodInfo> GetConcreteImplementations(Type? pipedType, Type[] typeArguments,
string? subCommand)
internal IEnumerable<MethodInfo> 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<MethodInfo> GetConcreteImplementationsInternal(Type? pipedType, Type[] typeArguments,
string? subCommand)
{
var impls = GetGenericImplementations()
.Where(x => x.GetCustomAttribute<CommandImplementationAttribute>()?.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<TakesPipedTypeAsGenericAttribute>())
expectedLen -= 1;
return typeArguments.Length == expectedLen;
}
return typeArguments.Length == 0;
})
.Select(x =>
{
try
{
if (x.IsGenericMethodDefinition)
{
if (x.HasCustomAttribute<TakesPipedTypeAsGenericAttribute>())
{
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<MethodInfo>().ToList();
}
internal List<MethodInfo> GetGenericImplementations()
{
var t = GetType();
var methods = t.GetMethods(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly |
BindingFlags.Instance);
return methods.Where(x => x.HasCustomAttribute<CommandImplementationAttribute>()).ToList();
}
internal bool TryGetImplementation(Type? pipedType, string? subCommand, Type[] typeArguments,
[NotNullWhen(true)] out Func<CommandInvocationArguments, object?>? impl)
{
return _implementors[subCommand ?? ""].TryGetImplementation(pipedType, typeArguments, out impl);
return GetType()
.GetMethods(MethodFlags)
.Where(x => x.HasCustomAttribute<CommandImplementationAttribute>());
}
}

View File

@@ -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.
/// </summary>
/// <remarks>This is automatically generated based on the type name unless overridden with <see cref="ToolshedCommandAttribute"/>.</remarks>
public string Name { get; }
public string Name { get; private set; } = default!;
/// <summary>
/// Whether or not this command has subcommands.
/// </summary>
public bool HasSubCommands { get; }
public bool HasSubCommands;
/// <summary>
/// The additional type parameters of this command, specifically which parsers to use.
/// </summary>
/// <remarks>Every type specified must either be <see cref="Type"/> itself or something implementing <see cref="IAsType{Type}"/> where T is Type.</remarks>
/// <remarks>Every type specified must be either be <see cref="TypeTypeParser"/> or must inherit from CustomTypeParser&lt;Type&gt;.</remarks>
public virtual Type[] TypeParameterParsers => Array.Empty<Type>();
internal bool HasTypeParameters => TypeParameterParsers.Length != 0;
/// <summary>
/// The list of all subcommands on this command.
/// The set of all subcommands on this command.
/// </summary>
public IEnumerable<string> Subcommands => _implementors.Keys.Where(x => x != "");
public IEnumerable<string> Subcommands => CommandImplementors.Keys;
/// <summary>
/// List of parameters for this command and all sub commands. Used for command help usage.
/// Dictionary(subCommand, List(pipedType, List(parameterType)))
/// </summary>
private readonly Dictionary<string, List<(Type?, List<Type>)>> _readonlyParameters;
internal readonly Dictionary<string, ToolshedCommandImplementor> CommandImplementors = new();
protected ToolshedCommand()
private readonly Dictionary<string, HashSet<Type>> _acceptedTypes = new();
protected internal ToolshedCommand()
{
var name = GetType().GetCustomAttribute<ToolshedCommandAttribute>()!.Name;
}
internal void Init()
{
var type = GetType();
var name = type.GetCustomAttribute<ToolshedCommandAttribute>()!.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<string, Type>> parameters = new();
foreach (var typeParser in TypeParameterParsers)
{
if (typeParser == typeof(TypeTypeParser))
continue;
if (!typeParser.IsAssignableTo(typeof(CustomTypeParser<Type>)))
throw new InvalidCommandImplementation($"{nameof(TypeParameterParsers)} element {typeParser} is not {nameof(TypeTypeParser)} or assignable to {typeof(CustomTypeParser<Type>).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<string>();
var hasNonSubCommands = false;
foreach (var impl in impls)
{
var myParams = new SortedDictionary<string, Type>();
var orderedParams = new List<Type>();
var hasInverted = false;
var hasCtx = false;
Type? pipeType = null;
argNames.Clear();
foreach (var param in impl.GetParameters())
{
var hasAnyAttribute = false;
if (param.HasCustomAttribute<CommandArgumentAttribute>())
{
if (param.Name == null || !argNames.Add(param.Name))
throw new InvalidCommandImplementation($"Command arguments must have a unique name");
hasAnyAttribute = true;
}
if (param.HasCustomAttribute<PipedArgumentAttribute>())
{
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<CommandInvertedAttribute>())
{
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<CommandInvocationContextAttribute>())
{
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<TakesPipedTypeAsGenericAttribute>();
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<CommandImplementationAttribute>() 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<CommandArgumentAttribute>() is not null)
{
if (myParams.TryAdd(param.Name!, param.ParameterType))
orderedParams.Add(param.ParameterType);
}
if (param.GetCustomAttribute<PipedArgumentAttribute>() 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<Type> AcceptedTypes(string? subCommand)
internal HashSet<Type> AcceptedTypes(string? subCommand)
{
return GetGenericImplementations()
.Where(x =>
x.ConsoleGetPipedArgument() is not null
&& x.GetCustomAttribute<CommandImplementationAttribute>()?.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<string, object?>? 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<CommandImplementationAttribute>() 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<string, object?> Arguments => Bundle.Arguments;
public Dictionary<string, object?>? Arguments => Bundle.Arguments;
public bool Inverted => Bundle.Inverted;
public Type? PipedArgumentType => Bundle.PipedArgumentType;
}
internal sealed class CommandArgumentBundle
/// <summary>
/// Collection of values used in the process of parsing a single command.
/// </summary>
public struct CommandArgumentBundle
{
public required Dictionary<string, object?> Arguments;
public required bool Inverted = false;
public required Type? PipedArgumentType;
public required Type[] TypeArguments;
/// <summary>
/// The name of the command currently being parsed.
/// </summary>
public string? Command;
/// <summary>
/// The name of the sub-command currently being parsed.
/// </summary>
public string? SubCommand;
/// <summary>
/// The collection of arguments that will be handed to the command method.
/// </summary>
public Dictionary<string, object?>? Arguments;
/// <summary>
/// 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 <see cref="PipedType"/>.
/// </summary>
public Type[]? TypeArguments;
/// <summary>
/// The value that will get passed to any method arguments with the <see cref="CommandInvertedAttribute"/>.
/// </summary>
public required bool Inverted;
/// <summary>
/// The type of input that will be piped into this command.
/// </summary>
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);

View File

@@ -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<Robust.Shared.Toolshed.CommandInvocationArguments, object?>;
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<CommandDiscriminator, Func<CommandInvocationArguments, object?>> Implementations = new();
/// <summary>
/// The full name of a command for use when fetching localized strings.
/// </summary>
public readonly string LocName;
public ToolshedCommandImplementor()
private readonly ToolshedManager _toolshed;
private readonly ILocalizationManager _loc;
public readonly Dictionary<CommandDiscriminator, Func<CommandInvocationArguments, object?>> Implementations = new();
/// <summary>
/// Cache for <see cref="TryGetConcreteMethod"/>.
/// </summary>
private readonly Dictionary<CommandDiscriminator, ConcreteCommandMethod?> _methodCache = new();
/// <summary>
/// All methods in <see cref="Owner"/> that correspond to the given <see cref="SubCommand"/>.
/// </summary>
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<CommandImplementationAttribute>() 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}";
}
/// <summary>
/// Attempt to parse the type-arguments and arguments and return an invocable expression.
/// </summary>
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;
}
/// <summary>
@@ -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.
/// </summary>
public bool TryParseArguments(
bool doAutocomplete,
ParserContext parserContext,
string? subCommand,
Type? pipedType,
[NotNullWhen(true)] out Dictionary<string, object?>? 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<MapLikeCommandAttribute>() 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<Type>();
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<string, object?>? 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<Type>();
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<Type> 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<T> 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<Type>) (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;
}
/// <summary>
/// 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.
/// </summary>
public bool TryGetImplementation(Type? pipedType, Type[] typeArguments, [NotNullWhen(true)] out Func<CommandInvocationArguments, object?>? 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<MethodInfo> 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<Expression>();
foreach (var param in unshimmed.GetParameters())
{
if (param.GetCustomAttribute<PipedArgumentAttribute>() 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<CommandArgumentAttribute>() is { } arg)
{
// (ParameterType)(args.Arguments[param.Name])
paramList.Add(Expression.Convert(
Expression.MakeIndex(
Expression.Property(args, nameof(CommandInvocationArguments.Arguments)),
typeof(Dictionary<string, object?>).FindIndexerProperty(),
new [] {Expression.Constant(param.Name)}),
param.ParameterType));
continue;
}
if (param.GetCustomAttribute<CommandInvertedAttribute>() is { } _)
{
// args.Inverted
paramList.Add(Expression.Property(args, nameof(CommandInvocationArguments.Inverted)));
continue;
}
if (param.GetCustomAttribute<CommandInvocationContextAttribute>() 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<Func<CommandInvocationArguments, object?>>(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<CommandArgumentAttribute>();
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);
}
/// <summary>
/// When a method has the <see cref="TakesPipedTypeAsGenericAttribute"/>, this method is used to actually
/// determine the generic type argument given the type of the piped in value.
/// </summary>
/// <param name="inputType">The type of value that was piped in</param>
/// <param name="parameterType">The type as specified in the method</param>
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);
}
/// <summary>
/// Attempts to fetch a callable shim for a command, aka it's implementation, using the given types.
/// </summary>
public Func<CommandInvocationArguments, object?> 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<CommandInvocationArguments, object?> GetImplementationInternal(CommandArgumentBundle cmdArgs, ConcreteCommandMethod method)
{
var args = Expression.Parameter(typeof(CommandInvocationArguments));
var paramList = new List<Expression>();
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<Func<CommandInvocationArguments, object?>>(partialShim, args).Compile();
}
private Expression GetParamExpr(ParameterInfo param, Type? pipedType, ParameterExpression args)
{
if (param.HasCustomAttribute<PipedArgumentAttribute>())
{
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<CommandInvertedAttribute>())
{
// args.Inverted
return Expression.Property(args, nameof(CommandInvocationArguments.Inverted));
}
if (param.HasCustomAttribute<CommandArgumentAttribute>())
return GetArgExpr(param, args);
if (param.HasCustomAttribute<CommandInvocationContextAttribute>()
|| 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<string, object?>).FindIndexerProperty(),
new[] {Expression.Constant(param.Name)});
// args.Context
var ctx = Expression.Property(args, nameof(CommandInvocationArguments.Context));
// ValueRef<T>.TryEvaluate
var evalMethod = typeof(ValueRef<>)
.MakeGenericType(param.ParameterType)
.GetMethod(nameof(ValueRef<int>.EvaluateParameter), BindingFlags.Static | BindingFlags.NonPublic)!;
// ValueRef<T>.TryEvaluate(args.Arguments[param.Name], args.Context)
return Expression.Call(evalMethod, argValue, ctx);
}
public override string ToString()
{
return FullName;
}
/// <inheritdoc cref="ToolshedCommand.GetHelp"/>
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();
}
/// <inheritdoc cref="ToolshedCommand.DescriptionLocKey"/>
public string DescriptionLocKey() => $"command-description-{LocName}";
/// <inheritdoc cref="ToolshedCommand.Description"/>
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;
}
}
/// <summary>
/// Struct for caching information about a command's methods. Helps reduce LINQ & reflection calls when attempting
/// to find matching methods.
/// </summary>
internal sealed class CommandMethod
{
/// <summary>
/// The method associated with some command.
/// </summary>
public readonly MethodInfo Info;
/// <summary>
/// The argument associated with the piped value.
/// </summary>
public readonly ParameterInfo? PipeArg;
public readonly bool Generic;
public readonly bool Invertible;
/// <summary>
/// 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 <see cref="TakesPipedTypeAsGenericAttribute"/>.
/// </summary>
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<TakesPipedTypeAsGenericAttribute>();
}
}
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");
}
}

View File

@@ -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<string, ToolshedCommand> _commands = new();
// All commands, including subcommands.
private List<CommandSpec> _allCommands = new();
private readonly Dictionary<Type, List<CommandSpec>> _commandTypeMap = new();
private readonly Dictionary<Type, List<CommandSpec>> _commandPipeValueMap = new();
private readonly Dictionary<CommandSpec, HashSet<Type>> _commandReturnValueMap = new();
private readonly Dictionary<Type, CommandSpec[]> _commandTypeCache = new();
private readonly Dictionary<Type, CompletionOption[]> _commandCompletionCache = new();
private ISawmill _log = default!;
@@ -26,22 +36,9 @@ public sealed class ToolshedEnvironment
/// Provides every registered command, including subcommands.
/// </summary>
/// <returns>Enumerable of every command.</returns>
public IEnumerable<CommandSpec> AllCommands()
public IReadOnlyList<CommandSpec> 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;
}
/// <summary>
@@ -63,40 +60,31 @@ public sealed class ToolshedEnvironment
return _commands.TryGetValue(commandName, out command);
}
public ToolshedEnvironment(IEnumerable<Type> 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<IDependencyCollection>().InjectDependencies(command, oneOff: true);
_commands.Add(command.Name, command);
}
InitializeQueries();
}
/// <summary>
/// Initializes a default toolshed context.
/// </summary>
public ToolshedEnvironment()
{
IoCManager.InjectDependencies(this);
Init(_reflection.FindTypesWithAttribute<ToolshedCommandAttribute>());
}
/// <summary>
/// Initialized a toolshed context with only the specified toolshed commands.
/// </summary>
public ToolshedEnvironment(IEnumerable<Type> commands)
{
IoCManager.InjectDependencies(this);
Init(commands);
}
private void Init(IEnumerable<Type> commands)
{
_log = _logManager.GetSawmill("toolshed");
var watch = new Stopwatch();
watch.Start();
var tys = _reflection.FindTypesWithAttribute<ToolshedCommandAttribute>();
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<IDependencyCollection>().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<CommandSpec>();
_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<Type>(), 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}");
}
/// <summary>
/// Returns all commands that fit the given type constraints.
/// </summary>
/// <returns>Enumerable of matching command specs.</returns>
public IEnumerable<CommandSpec> CommandsFittingConstraint(Type input, Type output)
public bool TryGetCommands<T>([NotNullWhen(true)] out IReadOnlyList<CommandSpec>? 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<Type>(), subcommand);
foreach (var impl in impls)
{
if (impl.ReturnType.IsAssignableTo(output))
yield return new CommandSpec(command, subcommand);
}
}
commands = list;
return true;
}
/// <summary>
@@ -168,23 +139,55 @@ public sealed class ToolshedEnvironment
/// <param name="t">Type to use in the query.</param>
/// <returns>Enumerable of matching command specs.</returns>
/// <remarks>Not currently type constraint aware.</remarks>
public IEnumerable<CommandSpec> 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<CommandSpec>(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, "<command>");
}
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, "<command>");
}
/// <summary>
@@ -198,13 +201,7 @@ public sealed class ToolshedEnvironment
private HashSet<Type> GetCommandRetValuesInternal(CommandSpec command)
{
if (!_commandReturnValueMap.TryGetValue(command, out var l))
{
l = new();
_commandReturnValueMap[command] = l;
}
return l;
return _commandReturnValueMap.GetOrNew(command);
}
private List<CommandSpec> 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);
}
}

View File

@@ -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<Type, ITypeParser?> _consoleTypeParsers = new();
private readonly Dictionary<ITypeParser, ITypeParser?> _argParsers = new();
private readonly Dictionary<Type, ITypeParser> _customParsers = new();
private readonly Dictionary<Type, Type> _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<ITypeParser>();
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;
}
/// <summary>
/// Variant of <see cref="GetParserForType"/> that will return a parser that also attempts to resolve a type from a
/// variable or block via the <see cref="ValueRef{T}"/> and <see cref="Block"/> parsers.
/// </summary>
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));
}
/// <summary>
/// Variant of <see cref="GetParserForType"/> that will return a parser that also attempts to resolve a type from a
/// variable or block via the <see cref="ValueRef{T}"/> parsers. If that fails, it will fall back to using the given
/// type parser
/// </summary>
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<TParser, T>() where TParser : CustomTypeParser<T>, new() where T : notnull
{
return (TParser)GetCustomParser(typeof(TParser));
}
/// <summary>
/// Attempt to fetch the custom parser instance of the given type.
/// </summary>
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
/// <param name="error">A console error, if any, that can be reported to explain the parsing failure.</param>
/// <typeparam name="T">The type to parse from the input.</typeparam>
/// <returns>Success.</returns>
public bool TryParse<T>(ParserContext parserContext, [NotNullWhen(true)] out T? parsed, out IConError? error)
public bool TryParse<T>(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;
}
/// <summary>
/// iunno man it does autocomplete what more do u want
/// </summary>
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);
}
/// <summary>
/// Attempts to parse the given type.
/// Attempts to parse the given type directly. Unlike <see cref="TryParseArgument"/> this will not attempt
/// to resolve variable or command blocks.
/// </summary>
/// <param name="parserContext">The input to parse from.</param>
/// <param name="t">The type to parse from the input.</param>
/// <param name="parsed">The parsed value, if any.</param>
/// <param name="error">A console error, if any, that can be reported to explain the parsing failure.</param>
/// <returns>Success.</returns>
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;
}
/// <summary>
/// Variant of <see cref="TryParse{T}"/> that will first attempt to parse the argument as a <see cref="ValueRef{T}"/>
/// or <see cref="Block"/>, before falling back to the default parser. Note that this generally does not directly
/// return the requested type.
/// </summary>
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; }

View File

@@ -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
/// </remarks>
public IPermissionController? ActivePermissionController { get; set; }
/// <summary>
/// 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.
/// </summary>
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

View File

@@ -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();
}
}

View File

@@ -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);
}
/// <summary>
/// Invokes a command with the given context.
/// </summary>
@@ -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;
}
}
/// <summary>
@@ -183,25 +200,31 @@ public readonly record struct CommandSpec(ToolshedCommand Cmd, string? SubComman
/// </summary>
public CompletionOption AsCompletion()
{
return new CompletionOption(
$"{Cmd.Name}{(SubCommand is not null ? ":" + SubCommand : "")}",
Cmd.Description(SubCommand)
);
return new CompletionOption(FullName(), Cmd.Description(SubCommand));
}
/// <summary>
/// Returns the full name of the command.
/// </summary>
public string FullName() => $"{Cmd.Name}{(SubCommand is not null ? ":" + SubCommand : "")}";
public string FullName() => SubCommand == null ? Cmd.Name : $"{Cmd.Name}:{SubCommand}";
/// <summary>
/// Returns the localization string for the description of this command.
/// </summary>
public string DescLocStr() => Cmd.UnlocalizedDescription(SubCommand);
public string DescLocStr() => Cmd.DescriptionLocKey(SubCommand);
/// <inheritdoc/>
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; }
}

View File

@@ -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;
/// <summary>
/// This custom type parser is used for parsing the type returned by a <see cref="Block{T}"/> command argument.
/// </summary>
public sealed class BlockOutputParser : CustomTypeParser<Type>
{
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.
/// <summary>
/// This custom type parser is used for parsing the type returned by a <see cref="Block{T,T}"/>, where the block's input
/// type is inferred from the type being piped into the command that is currently being parsed.
/// </summary>
/// <remarks>
/// If the piped type is an <see cref="IEnumerable{T}"/>, 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
/// <see cref="PipedArgumentAttribute"/> is also an <see cref="IEnumerable{T}"/>
/// </remarks>>
public sealed class MapBlockOutputParser : CustomTypeParser<Type>
{
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;
}
}

View File

@@ -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<Block>
{
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<T> : TypeParser<Block<T>>
{
public BlockTypeParser()
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block<T>? result)
{
return Block<T>.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<T>.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<T>.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<T>.TryParse(ctx, out _);
return ctx.Completions;
}
}
internal sealed class BlockTypeParser<TIn, TOut> : TypeParser<Block<TIn, TOut>>
{
public BlockTypeParser()
public override bool TryParse(ParserContext ctx, [NotNullWhen(true)] out Block<TIn, TOut>? result)
{
return Block<TIn, TOut>.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<TIn, TOut>.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<TIn, TOut>.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<TIn, TOut>.TryParse(ctx, out _);
return ctx.Completions;
}
}

View File

@@ -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<bool>
{
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"});
}
}

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