Skip to content

Commit

Permalink
Misc Toolshed tweaks (#4990)
Browse files Browse the repository at this point in the history
* Toolshed tweaks

* oops

* Apply suggestions from code review

Co-authored-by: Moony <[email protected]>

* Re-add NotImplementedException

* Move error message

---------

Co-authored-by: Moony <[email protected]>
Co-authored-by: metalgearsloth <[email protected]>
  • Loading branch information
3 people authored Mar 24, 2024
1 parent df0945f commit b4c1618
Show file tree
Hide file tree
Showing 10 changed files with 111 additions and 27 deletions.
3 changes: 3 additions & 0 deletions RELEASE-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,12 @@ END TEMPLATE-->

* Add a CompletionHelper for audio filepaths that handles server packaging.
* Add Random.NextAngle(min, max) method and Pick for `ValueList<T>`.
* Added an `ICommonSession` parser for toolshed commands.

### Bugfixes

* Fixed some issues where toolshed commands were generating completions for the wrong arguments

*None yet*

### Other
Expand Down
1 change: 1 addition & 0 deletions Resources/Locale/en-US/commands.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ cmd-parse-failure-uid = {$arg} is not a valid entity UID.
cmd-parse-failure-mapid = {$arg} is not a valid MapId.
cmd-parse-failure-grid = {$arg} is not a valid grid.
cmd-parse-failure-entity-exist = UID {$arg} does not correspond to an existing entity.
cmd-parse-failure-session = There is no session with username: {$username}
cmd-error-file-not-found = Could not find file: {$file}.
cmd-error-dir-not-found = Could not find directory: {$dir}.
Expand Down
4 changes: 4 additions & 0 deletions Robust.Shared/Toolshed/Syntax/Expression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ public static bool TryParse(bool doAutocomplete,

if (parserContext.EatTerminator())
break;

// Prevent auto completions from dumping a list of all commands at the end of any complete command.
if (parserContext.Index > parserContext.MaxIndex)
break;
}

if (error is OutOfInputError && noCommand)
Expand Down
2 changes: 1 addition & 1 deletion Robust.Shared/Toolshed/ToolshedCommand.Entities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ protected bool HasComp<T>(EntityUid entityUid)
/// A shorthand for attempting to retrieve the given component for an entity.
/// </summary>
[PublicAPI, MethodImpl(MethodImplOptions.AggressiveInlining)]
protected bool TryComp<T>(EntityUid? entity, [NotNullWhen(true)] out T? component)
protected bool TryComp<T>([NotNullWhen(true)] EntityUid? entity, [NotNullWhen(true)] out T? component)
where T: IComponent
=> EntityManager.TryGetComponent(entity, out component);

Expand Down
38 changes: 18 additions & 20 deletions Robust.Shared/Toolshed/ToolshedCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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;
Expand Down Expand Up @@ -47,6 +48,7 @@ namespace Robust.Shared.Toolshed;
public abstract partial class ToolshedCommand
{
[Dependency] protected readonly ToolshedManager Toolshed = default!;
[Dependency] protected readonly ILocalizationManager Loc = default!;

/// <summary>
/// The user-facing name of the command.
Expand Down Expand Up @@ -99,7 +101,7 @@ protected ToolshedCommand()
};

var impls = GetGenericImplementations();
Dictionary<string, SortedDictionary<string, Type>> parameters = new();
Dictionary<(string, Type?), SortedDictionary<string, Type>> parameters = new();

foreach (var impl in impls)
{
Expand All @@ -117,27 +119,26 @@ protected ToolshedCommand()
};
}

Type? pipedType = null;
foreach (var param in impl.GetParameters())
{
if (param.GetCustomAttribute<CommandArgumentAttribute>() is not null)
{
if (parameters.ContainsKey(param.Name!))
continue;

myParams.Add(param.Name!, param.ParameterType);
}
}
myParams.TryAdd(param.Name!, param.ParameterType);

if (parameters.TryGetValue(subCmd ?? "", out var existing))
{
if (!existing.SequenceEqual(existing))
if (param.GetCustomAttribute<PipedArgumentAttribute>() is not null)
{
throw new NotImplementedException("All command implementations of a given subcommand must share the same parameters!");
if (pipedType != null)
throw new NotSupportedException($"Commands cannot have more than one piped argument");
pipedType = param.ParameterType;
}
}
else
parameters.Add(subCmd ?? "", myParams);

var key = (subCmd ?? "", pipedType);
if (parameters.TryAdd(key, myParams))
continue;

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

Expand Down Expand Up @@ -184,14 +185,11 @@ internal sealed class CommandArgumentBundle
public required Type[] TypeArguments;
}

internal readonly record struct CommandDiscriminator(Type? PipedType, Type[] TypeArguments) : IEquatable<CommandDiscriminator?>
internal readonly record struct CommandDiscriminator(Type? PipedType, Type[] TypeArguments)
{
public bool Equals(CommandDiscriminator? other)
public bool Equals(CommandDiscriminator other)
{
if (other is not {} value)
return false;

return value.PipedType == PipedType && value.TypeArguments.SequenceEqual(TypeArguments);
return other.PipedType == PipedType && other.TypeArguments.SequenceEqual(TypeArguments);
}

public override int GetHashCode()
Expand Down
17 changes: 14 additions & 3 deletions Robust.Shared/Toolshed/ToolshedCommandImplementor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public bool TryParseArguments(
return false;
}

autocomplete = null;
args = new();
foreach (var argument in impl.ConsoleGetArguments())
{
Expand All @@ -124,19 +125,29 @@ public bool TryParseArguments(
{
error?.Contextualize(parserContext.Input, (start, parserContext.Index));
args = null;
autocomplete = null;
if (doAutocomplete)

// 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);
}
return false;
}
args[argument.Name!] = parsed;

if (!doAutocomplete || parserContext.Index <= parserContext.MaxIndex)
continue;

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

error = null;
autocomplete = null;
return true;
}

Expand Down
1 change: 0 additions & 1 deletion Robust.Shared/Toolshed/TypeParsers/EntityTypeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.GameObjects;
using Robust.Shared.IoC;
using Robust.Shared.Maths;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Toolshed.Syntax;
Expand Down
2 changes: 2 additions & 0 deletions Robust.Shared/Toolshed/TypeParsers/PrototypeTypeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public override bool TryParse(ParserContext parserContext, [NotNullWhen(true)] o
public readonly record struct Prototype<T>(T Value) : IAsType<string>
where T : class, IPrototype
{
public ProtoId<T> Id => Value.ID;

public string AsType()
{
return Value.ID;
Expand Down
64 changes: 64 additions & 0 deletions Robust.Shared/Toolshed/TypeParsers/SessionTypeParser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Maths;
using Robust.Shared.Player;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Toolshed.Syntax;
using Robust.Shared.Utility;

namespace Robust.Shared.Toolshed.TypeParsers;

/// <summary>
/// Parse a username to an <see cref="ICommonSession"/>
/// </summary>
internal sealed class SessionTypeParser : TypeParser<ICommonSession>
{
[Dependency] private ISharedPlayerManager _player = default!;

public override bool TryParse(ParserContext parser, [NotNullWhen(true)] out object? result, out IConError? error)
{
var start = parser.Index;
var word = parser.GetWord();
error = null;
result = null;

if (word == null)
{
error = new OutOfInputError();
return false;
}

if (_player.TryGetSessionByUsername(word, out var session))
{
result = session;
return true;
}

error = new InvalidUsername(Loc, word);
error.Contextualize(parser.Input, (start, parser.Index));
return false;
}

public override async ValueTask<(CompletionResult? result, IConError? error)> TryAutocomplete(ParserContext parserContext,
string? argName)
{
var opts = CompletionHelper.SessionNames(true, _player);
return (CompletionResult.FromHintOptions(opts, "<player session>"), null);
}

public record InvalidUsername(ILocalizationManager Loc, string Username) : IConError
{
public FormattedMessage DescribeInner()
{
return FormattedMessage.FromMarkup(Loc.GetString("cmd-parse-failure-session", ("username", Username)));
}

public string? Expression { get; set; }
public Vector2i? IssueSpan { get; set; }
public StackTrace? Trace { get; set; }
}
}
6 changes: 4 additions & 2 deletions Robust.Shared/Toolshed/TypeParsers/TypeParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using JetBrains.Annotations;
using Robust.Shared.Console;
using Robust.Shared.IoC;
using Robust.Shared.Localization;
using Robust.Shared.Log;
using Robust.Shared.Toolshed.Errors;
using Robust.Shared.Toolshed.Syntax;
Expand All @@ -26,8 +27,9 @@ public abstract class TypeParser<T> : ITypeParser
where T: notnull
{
[Dependency] private readonly ILogManager _log = default!;
[Dependency] protected readonly ILocalizationManager Loc = default!;

protected ISawmill _sawmill = default!;
protected ISawmill Log = default!;

public virtual Type Parses => typeof(T);

Expand All @@ -37,6 +39,6 @@ public abstract class TypeParser<T> : ITypeParser

public virtual void PostInject()
{
_sawmill = _log.GetSawmill(GetType().PrettyName());
Log = _log.GetSawmill(GetType().PrettyName());
}
}

0 comments on commit b4c1618

Please sign in to comment.