diff --git a/SecretAPI.CodeGeneration/AnalyzerReleases.Shipped.md b/SecretAPI.CodeGeneration/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/SecretAPI.CodeGeneration/AnalyzerReleases.Shipped.md @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/AnalyzerReleases.Unshipped.md b/SecretAPI.CodeGeneration/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..c302cdd --- /dev/null +++ b/SecretAPI.CodeGeneration/AnalyzerReleases.Unshipped.md @@ -0,0 +1,5 @@ +### New Rules + + Rule ID | Category | Severity | Notes +------------|----------|----------|--------------------- + SecretGen0 | Usage | Error | CA6000_AnalyzerName \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/CodeBuilders/ClassBuilder.cs b/SecretAPI.CodeGeneration/CodeBuilders/ClassBuilder.cs new file mode 100644 index 0000000..42aa391 --- /dev/null +++ b/SecretAPI.CodeGeneration/CodeBuilders/ClassBuilder.cs @@ -0,0 +1,60 @@ +namespace SecretAPI.CodeGeneration.CodeBuilders; + +internal class ClassBuilder : CodeBuilder +{ + private NamespaceDeclarationSyntax _namespaceDeclaration; + private ClassDeclarationSyntax _classDeclaration; + + private readonly List _usings = new(); + private readonly List _methods = new(); + + private ClassBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDeclarationSyntax classDeclaration) + { + _namespaceDeclaration = namespaceDeclaration; + _classDeclaration = classDeclaration; + + AddUsingStatements("System.CodeDom.Compiler"); + } + + internal static ClassBuilder CreateBuilder(INamedTypeSymbol namedClass) + => CreateBuilder(NamespaceDeclaration(ParseName(namedClass.ContainingNamespace.ToDisplayString())), ClassDeclaration(namedClass.Name)); + + internal static ClassBuilder CreateBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDeclarationSyntax classDeclaration) + => new(namespaceDeclaration, classDeclaration); + + internal ClassBuilder AddUsingStatements(params string[] usingStatements) + { + foreach (string statement in usingStatements) + { + UsingDirectiveSyntax usings = UsingDirective(ParseName(statement)); + if (!_usings.Any(existing => existing.IsEquivalentTo(usings))) + _usings.Add(usings); + } + + return this; + } + + internal MethodBuilder StartMethodCreation(string methodName, TypeSyntax returnType) => new(this, methodName, returnType); + internal MethodBuilder StartMethodCreation(string methodName, SyntaxKind returnType) => StartMethodCreation(methodName, GetPredefinedTypeSyntax(returnType)); + + internal void AddMethodDefinition(MethodDeclarationSyntax method) => _methods.Add(method); + + internal CompilationUnitSyntax Build() + { + _classDeclaration = _classDeclaration + .AddAttributeLists(GetGeneratedCodeAttributeListSyntax()) + .AddModifiers(_modifiers.ToArray()) + .AddMembers(_methods.Cast().ToArray()); + + _namespaceDeclaration = _namespaceDeclaration + .AddUsings(_usings.ToArray()) + .AddMembers(_classDeclaration); + + return CompilationUnit() + .AddMembers(_namespaceDeclaration) + .NormalizeWhitespace() + .WithLeadingTrivia(Comment("// "), LineFeed, Comment("#pragma warning disable"), LineFeed, LineFeed); + } + + internal void Build(SourceProductionContext context, string name) => context.AddSource(name, Build().ToFullString()); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/CodeBuilders/CodeBuilder.cs b/SecretAPI.CodeGeneration/CodeBuilders/CodeBuilder.cs new file mode 100644 index 0000000..a5110ae --- /dev/null +++ b/SecretAPI.CodeGeneration/CodeBuilders/CodeBuilder.cs @@ -0,0 +1,19 @@ +namespace SecretAPI.CodeGeneration.CodeBuilders; + +/// +/// Base of a code builder. +/// +/// The this is handling. +internal abstract class CodeBuilder + where TCodeBuilder : CodeBuilder +{ + protected readonly List _modifiers = new(); + + internal TCodeBuilder AddModifiers(params SyntaxKind[] modifiers) + { + foreach (SyntaxKind token in modifiers) + _modifiers.Add(Token(token)); + + return (TCodeBuilder)this; + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/CodeBuilders/MethodBuilder.cs b/SecretAPI.CodeGeneration/CodeBuilders/MethodBuilder.cs new file mode 100644 index 0000000..6d771dc --- /dev/null +++ b/SecretAPI.CodeGeneration/CodeBuilders/MethodBuilder.cs @@ -0,0 +1,44 @@ +namespace SecretAPI.CodeGeneration.CodeBuilders; + +internal class MethodBuilder : CodeBuilder +{ + private readonly ClassBuilder _classBuilder; + private readonly List _parameters = new(); + private readonly List _statements = new(); + private readonly string _methodName; + private readonly TypeSyntax _returnType; + + internal MethodBuilder(ClassBuilder classBuilder, string methodName, TypeSyntax returnType) + { + _classBuilder = classBuilder; + _methodName = methodName; + _returnType = returnType; + } + + internal MethodBuilder AddStatements(params StatementSyntax[] statements) + { + _statements.AddRange(statements); + return this; + } + + internal MethodBuilder AddParameters(params MethodParameter[] parameters) + { + foreach (MethodParameter parameter in parameters) + _parameters.Add(parameter.Syntax); + + return this; + } + + internal ClassBuilder FinishMethodBuild() + { + BlockSyntax body = _statements.Any() ? Block(_statements) : Block(); + + MethodDeclarationSyntax methodDeclaration = MethodDeclaration(_returnType, _methodName) + .AddModifiers(_modifiers.ToArray()) + .AddParameterListParameters(_parameters.ToArray()) + .WithBody(body); + + _classBuilder.AddMethodDefinition(methodDeclaration); + return _classBuilder; + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Diagnostics/CommandDiagnostics.cs b/SecretAPI.CodeGeneration/Diagnostics/CommandDiagnostics.cs new file mode 100644 index 0000000..61a3cb3 --- /dev/null +++ b/SecretAPI.CodeGeneration/Diagnostics/CommandDiagnostics.cs @@ -0,0 +1,12 @@ +namespace SecretAPI.CodeGeneration.Diagnostics; + +internal static class CommandDiagnostics +{ + internal static readonly DiagnosticDescriptor InvalidExecuteMethod = new( + "SecretGen0", + "Invalid ExecuteCommand method", + "Method '{0}' marked with [ExecuteCommand] is invalid: {1}", + "Usage", + DiagnosticSeverity.Error, + true); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Generators/CallOnLoadGenerator.cs b/SecretAPI.CodeGeneration/Generators/CallOnLoadGenerator.cs new file mode 100644 index 0000000..e9b05ac --- /dev/null +++ b/SecretAPI.CodeGeneration/Generators/CallOnLoadGenerator.cs @@ -0,0 +1,122 @@ +namespace SecretAPI.CodeGeneration.Generators; + +/// +/// Code generator for CallOnLoad/CallOnUnload +/// +[Generator] +public class CallOnLoadGenerator : IIncrementalGenerator +{ + private const string PluginNamespace = "LabApi.Loader.Features.Plugins"; + private const string PluginBaseClassName = "Plugin"; + private const string CallOnLoadAttributeLocation = "SecretAPI.Attributes.CallOnLoadAttribute"; + private const string CallOnUnloadAttributeLocation = "SecretAPI.Attributes.CallOnUnloadAttribute"; + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider methodProvider = + context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => node is MethodDeclarationSyntax { AttributeLists.Count: > 0 }, + static (ctx, _) => + ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as IMethodSymbol) + .Where(static m => m is not null)!; + + IncrementalValuesProvider<(IMethodSymbol method, bool isLoad, bool isUnload)> callProvider = + methodProvider.Select(static (method, _) => ( + method, + HasAttribute(method, CallOnLoadAttributeLocation), + HasAttribute(method, CallOnUnloadAttributeLocation))) + .Where(static m => m.Item2 || m.Item3); + + IncrementalValuesProvider pluginClassProvider = + context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax, + static (ctx, _) => + ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol) + .Where(static c => c != null && !c.IsAbstract && c.BaseType?.Name == PluginBaseClassName && + c.BaseType.ContainingNamespace.ToDisplayString() == PluginNamespace); + + context.RegisterSourceOutput(pluginClassProvider.Combine(callProvider.Collect()), static (context, data) => + { + Generate(context, data.Left, data.Right); + }); + } + + private static bool HasAttribute(IMethodSymbol? method, string attributeLocation) + { + if (method == null) + return false; + + foreach (AttributeData attribute in method.GetAttributes()) + { + if (attribute.AttributeClass?.ToDisplayString() == attributeLocation) + return true; + } + + return false; + } + + private static int GetPriority(IMethodSymbol method, string attributeLocation) + { + AttributeData? attribute = method.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == attributeLocation); + if (attribute == null) + return 0; + + if (attribute.ConstructorArguments.Length > 0) + return (int)attribute.ConstructorArguments[0].Value!; + + return 0; + } + + private static bool ShouldAutogenerate(IMethodSymbol method, string attributeLocation) + { + AttributeData? attribute = method.GetAttributes() + .FirstOrDefault(a => a.AttributeClass?.ToDisplayString() == attributeLocation); + + if (attribute is { ConstructorArguments.Length: >= 2 }) + return (bool)attribute.ConstructorArguments[1].Value!; + + return false; + } + + private static void Generate( + SourceProductionContext context, + INamedTypeSymbol? pluginClassSymbol, + ImmutableArray<(IMethodSymbol method, bool isLoad, bool isUnload)> methods) + { + if (pluginClassSymbol == null || methods.IsEmpty) + return; + + IMethodSymbol[] loadCalls = methods + .Where(m => m.isLoad && ShouldAutogenerate(m.method, CallOnLoadAttributeLocation)) + .Select(m => m.method) + .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation)) + .ToArray(); + + IMethodSymbol[] unloadCalls = methods + .Where(m => m.isUnload && ShouldAutogenerate(m.method, CallOnUnloadAttributeLocation)) + .Select(m => m.method) + .OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation)) + .ToArray(); + + if (!loadCalls.Any() && !unloadCalls.Any()) + return; + + ClassBuilder classBuilder = ClassBuilder.CreateBuilder(pluginClassSymbol) + .AddUsingStatements("System") + .AddModifiers(SyntaxKind.PartialKeyword); + + classBuilder.StartMethodCreation("OnLoad", SyntaxKind.VoidKeyword) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddStatements(MethodCallStatements(loadCalls)) + .FinishMethodBuild(); + + classBuilder.StartMethodCreation("OnUnload", SyntaxKind.VoidKeyword) + .AddModifiers(SyntaxKind.PublicKeyword) + .AddStatements(MethodCallStatements(unloadCalls)) + .FinishMethodBuild(); + + classBuilder.Build(context, $"{pluginClassSymbol.Name}.g.cs"); + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Generators/CustomCommandGenerator.cs b/SecretAPI.CodeGeneration/Generators/CustomCommandGenerator.cs new file mode 100644 index 0000000..a80113a --- /dev/null +++ b/SecretAPI.CodeGeneration/Generators/CustomCommandGenerator.cs @@ -0,0 +1,157 @@ +namespace SecretAPI.CodeGeneration.Generators; + +/// +/// Code generator for custom commands, creating validation etc. +/// +[Generator] +public class CustomCommandGenerator : IIncrementalGenerator +{ + private const string CommandName = "CustomCommand"; + private const string ExecuteMethodName = "Execute"; + private const string ExecuteCommandMethodAttributeLocation = "SecretAPI.Features.Commands.Attributes.ExecuteCommandAttribute"; + private const string CommandResultLocation = "CommandResult"; + + private static readonly MethodParameter ArgumentsParam = + new( + identifier: "arguments", + type: GetSingleGenericTypeSyntax("ArraySegment", SyntaxKind.StringKeyword) + ); + + private static readonly MethodParameter SenderParam = + new( + identifier: "sender", + type: IdentifierName("ICommandSender") + ); + + private static readonly MethodParameter ResponseParam = + new( + identifier: "response", + type: GetPredefinedTypeSyntax(SyntaxKind.StringKeyword), + modifiers: TokenList( + Token(SyntaxKind.OutKeyword)) + ); + + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + IncrementalValuesProvider<(INamedTypeSymbol?, ImmutableArray)> classProvider + = context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax, + static (ctx, cancel) => + { + ClassDeclarationSyntax classSyntax = (ClassDeclarationSyntax)ctx.Node; + INamedTypeSymbol? typeSymbol = ModelExtensions.GetDeclaredSymbol(ctx.SemanticModel, classSyntax, cancel) as INamedTypeSymbol; + return (typeSymbol, GetExecuteMethods(ctx, classSyntax)); + }).Where(tuple => tuple is { typeSymbol: not null, Item2.IsEmpty: false }); + + context.RegisterSourceOutput(classProvider, (ctx, tuple) => Generate(ctx, tuple.Item1!, tuple.Item2)); + } + + private static ImmutableArray GetExecuteMethods( + GeneratorSyntaxContext context, + ClassDeclarationSyntax classDeclarationSyntax) + { + List methods = new(); + foreach (MethodDeclarationSyntax method in classDeclarationSyntax.Members.OfType()) + { + if (!IsExecuteMethod(context, method)) + continue; + + methods.Add(method); + } + + return methods.ToImmutableArray(); + } + + private static bool IsExecuteMethod(GeneratorSyntaxContext context, MethodDeclarationSyntax methodDeclarationSyntax) + { + foreach (AttributeListSyntax attributeListSyntax in methodDeclarationSyntax.AttributeLists) + { + foreach (AttributeSyntax attributeSyntax in attributeListSyntax.Attributes) + { + ITypeSymbol? attributeTypeSymbol = ModelExtensions.GetTypeInfo(context.SemanticModel, attributeSyntax).Type; + if (attributeTypeSymbol != null && attributeTypeSymbol.ToDisplayString() == ExecuteCommandMethodAttributeLocation) + return true; + } + } + + return false; + } + + private static void Generate( + SourceProductionContext context, + INamedTypeSymbol namedClassSymbol, + ImmutableArray executeMethods) + { + if (namedClassSymbol.IsAbstract) + return; + + if (namedClassSymbol.BaseType?.Name != CommandName) + return; + + ClassBuilder classBuilder = ClassBuilder.CreateBuilder(namedClassSymbol) + .AddUsingStatements("System", "System.Collections.Generic") + .AddUsingStatements("CommandSystem") + .AddModifiers(SyntaxKind.PartialKeyword); + + foreach (MethodDeclarationSyntax method in executeMethods) + { + if (method.ReturnType.ToString() != CommandResultLocation) + { + context.ReportDiagnostic( + Diagnostic.Create( + CommandDiagnostics.InvalidExecuteMethod, + method.ReturnType.GetLocation(), + method.Identifier.Text, + "Return type should be of type " + CommandResultLocation + ) + ); + } + } + + classBuilder.StartMethodCreation(ExecuteMethodName, SyntaxKind.BoolKeyword) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .AddParameters(ArgumentsParam, SenderParam, ResponseParam) + .FinishMethodBuild(); + + classBuilder.Build(context, $"{namedClassSymbol.Name}.g.cs"); + } +} + +// ! Example of basic structure needed + +/* + +ValidatorSingleton.Instance.Validate(arguments.First()); +ValidatorSingleton>.Instance.Validate(arguments.First()); + +// +#pragma warning disable + +namespace SecretAPI.Examples.Commands +{ + using System.CodeDom.Compiler; + using System; + using System.Collections.Generic; + using System.Linq; + using CommandSystem; + using Features.Commands; + + partial class ExampleParentCommand + { + public override bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + if (arguments.Any()) + { + string argument = arguments.First(); + foreach (CustomCommand subCommand in SubCommands) + { + if (argument == subCommand.Command || subCommand.Aliases.Any(a => a == argument)) + subCommand.Execute(arguments, sender, out response); + } + } + } + } +} + +*/ \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/GlobalUsings.cs b/SecretAPI.CodeGeneration/GlobalUsings.cs new file mode 100644 index 0000000..d0cdcf7 --- /dev/null +++ b/SecretAPI.CodeGeneration/GlobalUsings.cs @@ -0,0 +1,20 @@ +//? Utils from other places +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; +global using System.Collections.Immutable; + +//? Static utils from other places +global using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +global using static Microsoft.CodeAnalysis.CSharp.SyntaxFacts; + +//? Utils from SecretAPI +global using SecretAPI.CodeGeneration.CodeBuilders; +global using SecretAPI.CodeGeneration.Utils; +global using SecretAPI.CodeGeneration.Diagnostics; + +//? Static utils from SecretAPI +global using static SecretAPI.CodeGeneration.Utils.GenericTypeUtils; +global using static SecretAPI.CodeGeneration.Utils.GeneratedIdentifyUtils; +global using static SecretAPI.CodeGeneration.Utils.MethodUtils; +global using static SecretAPI.CodeGeneration.Utils.TypeUtils; \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/SecretAPI.CodeGeneration.csproj b/SecretAPI.CodeGeneration/SecretAPI.CodeGeneration.csproj new file mode 100644 index 0000000..4423fec --- /dev/null +++ b/SecretAPI.CodeGeneration/SecretAPI.CodeGeneration.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + 10 + enable + enable + + + + true + false + Library + true + + + + + + + + diff --git a/SecretAPI.CodeGeneration/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.CodeGeneration/Utils/GeneratedIdentifyUtils.cs new file mode 100644 index 0000000..3c7b30e --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/GeneratedIdentifyUtils.cs @@ -0,0 +1,19 @@ +namespace SecretAPI.CodeGeneration.Utils; + +internal static class GeneratedIdentifyUtils +{ + private static AttributeSyntax GetGeneratedCodeAttributeSyntax() + => Attribute(IdentifierName("GeneratedCode")) + .WithArgumentList( + AttributeArgumentList( + SeparatedList( + new SyntaxNodeOrToken[] + { + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.CodeGeneration"))), + Token(SyntaxKind.CommaToken), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("1.0.0"))), + }))); + + internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax() + => AttributeList(SingletonSeparatedList(GetGeneratedCodeAttributeSyntax())); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/GenericTypeUtils.cs b/SecretAPI.CodeGeneration/Utils/GenericTypeUtils.cs new file mode 100644 index 0000000..d4588bd --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/GenericTypeUtils.cs @@ -0,0 +1,12 @@ +namespace SecretAPI.CodeGeneration.Utils; + +internal static class GenericTypeUtils +{ + internal static TypeSyntax GetSingleGenericTypeSyntax(string genericName, SyntaxKind predefinedType) + => GenericName(genericName) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + PredefinedType( + Token(predefinedType))))); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/MethodParameter.cs b/SecretAPI.CodeGeneration/Utils/MethodParameter.cs new file mode 100644 index 0000000..b400ff2 --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/MethodParameter.cs @@ -0,0 +1,40 @@ +namespace SecretAPI.CodeGeneration.Utils; + +/// +/// Represents a method parameter used during code generation. +/// +internal readonly struct MethodParameter +{ + private readonly SyntaxList _attributeLists; + private readonly SyntaxTokenList _modifiers; + private readonly TypeSyntax? _type; + private readonly SyntaxToken _identifier; + private readonly EqualsValueClauseSyntax? _default; + + /// + /// Creates a new instance of . + /// + /// The name of the parameter. + /// The parameter type. May be for implicitly-typed parameters. + /// Optional parameter modifiers (e.g. ref, out, in). + /// Optional attribute lists applied to the parameter. + /// Optional default value. + internal MethodParameter( + string identifier, + TypeSyntax? type = null, + SyntaxTokenList modifiers = default, + SyntaxList attributeLists = default, + EqualsValueClauseSyntax? @default = null) + { + _identifier = IsValidIdentifier(identifier) + ? Identifier(identifier) + : throw new ArgumentException("Identifier is not valid.", nameof(identifier)); + + _type = type; + _modifiers = modifiers; + _attributeLists = attributeLists; + _default = @default; + } + + public ParameterSyntax Syntax => Parameter(_attributeLists, _modifiers, _type, _identifier, _default); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/MethodUtils.cs b/SecretAPI.CodeGeneration/Utils/MethodUtils.cs new file mode 100644 index 0000000..eec2737 --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/MethodUtils.cs @@ -0,0 +1,20 @@ +namespace SecretAPI.CodeGeneration.Utils; + +internal static class MethodUtils +{ + internal static StatementSyntax MethodCallStatement(string typeName, string methodName) => + MethodCallStatement(ParseTypeName(typeName), IdentifierName(methodName)); + + internal static StatementSyntax MethodCallStatement(TypeSyntax type, IdentifierNameSyntax method) + => ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + type, method))); + + internal static StatementSyntax[] MethodCallStatements(IMethodSymbol[] methodCalls) + { + IEnumerable statements = methodCalls.Select(s => MethodCallStatement(s.ContainingType.ToDisplayString(), s.Name)); + return statements.ToArray(); + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/TypeUtils.cs b/SecretAPI.CodeGeneration/Utils/TypeUtils.cs new file mode 100644 index 0000000..6bfbfcd --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/TypeUtils.cs @@ -0,0 +1,7 @@ +namespace SecretAPI.CodeGeneration.Utils; + +internal static class TypeUtils +{ + internal static PredefinedTypeSyntax GetPredefinedTypeSyntax(SyntaxKind kind) + => PredefinedType(Token(kind)); +} \ No newline at end of file diff --git a/SecretAPI.Examples/Commands/ExampleExplodeCommand.cs b/SecretAPI.Examples/Commands/ExampleExplodeCommand.cs new file mode 100644 index 0000000..5d9e1eb --- /dev/null +++ b/SecretAPI.Examples/Commands/ExampleExplodeCommand.cs @@ -0,0 +1,27 @@ +namespace SecretAPI.Examples.Commands +{ + using LabApi.Features.Console; + using LabApi.Features.Wrappers; + using SecretAPI.Features.Commands; + using SecretAPI.Features.Commands.Attributes; + + /// + /// An example subcommand for . + /// + public partial class ExampleExplodeCommand : CustomCommand + { + /// + public override string Command => "explode"; + + /// + public override string Description => "Explodes a player!"; + + [ExecuteCommand] + private CommandResult Explode([CommandSender] Player sender, Player target) + { + Logger.Debug($"Example explode command run by {sender.Nickname} - Target: {target.Nickname}"); + TimedGrenadeProjectile.SpawnActive(target.Position, ItemType.GrenadeHE, sender); + return new CommandResult(true, "Success"); + } + } +} \ No newline at end of file diff --git a/SecretAPI.Examples/Commands/ExampleParentCommand.cs b/SecretAPI.Examples/Commands/ExampleParentCommand.cs new file mode 100644 index 0000000..d8a55c3 --- /dev/null +++ b/SecretAPI.Examples/Commands/ExampleParentCommand.cs @@ -0,0 +1,33 @@ +namespace SecretAPI.Examples.Commands +{ + using System; + using LabApi.Features.Console; + using LabApi.Features.Wrappers; + using SecretAPI.Features.Commands; + using SecretAPI.Features.Commands.Attributes; + + /// + /// An example of a that explodes a player. + /// + public partial class ExampleParentCommand : CustomCommand + { + /// + public override string Command => "exampleparent"; + + /// + public override string Description => "Example of a parent command, handling some sub commands."; + + /// + public override string[] Aliases { get; } = []; + + /// + public override CustomCommand[] SubCommands { get; } = [new ExampleExplodeCommand()]; + + [ExecuteCommand] + private CommandResult Run([CommandSender] Player sender) + { + Logger.Debug($"Example parent was run by {sender.Nickname}"); + return new CommandResult(true, "Success"); + } + } +} \ No newline at end of file diff --git a/SecretAPI.Examples/SecretAPI.Examples.csproj b/SecretAPI.Examples/SecretAPI.Examples.csproj index 076e87e..4f7e537 100644 --- a/SecretAPI.Examples/SecretAPI.Examples.csproj +++ b/SecretAPI.Examples/SecretAPI.Examples.csproj @@ -8,13 +8,13 @@ - + diff --git a/SecretAPI.sln b/SecretAPI.sln index 4ed659a..4ab044c 100644 --- a/SecretAPI.sln +++ b/SecretAPI.sln @@ -4,6 +4,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI", "SecretAPI\Secr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI.Examples", "SecretAPI.Examples\SecretAPI.Examples.csproj", "{0064C982-5FE1-4B65-82F9-2EEF85651188}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SecretAPI.CodeGeneration", "SecretAPI.CodeGeneration\SecretAPI.CodeGeneration.csproj", "{8A490E06-9D85-43B5-A886-5B5BB14172D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -18,5 +20,9 @@ Global {0064C982-5FE1-4B65-82F9-2EEF85651188}.Debug|Any CPU.Build.0 = Debug|Any CPU {0064C982-5FE1-4B65-82F9-2EEF85651188}.Release|Any CPU.ActiveCfg = Release|Any CPU {0064C982-5FE1-4B65-82F9-2EEF85651188}.Release|Any CPU.Build.0 = Release|Any CPU + {8A490E06-9D85-43B5-A886-5B5BB14172D9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8A490E06-9D85-43B5-A886-5B5BB14172D9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A490E06-9D85-43B5-A886-5B5BB14172D9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8A490E06-9D85-43B5-A886-5B5BB14172D9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SecretAPI/Attributes/CallOnLoadAttribute.cs b/SecretAPI/Attributes/CallOnLoadAttribute.cs index 32bcd34..903df53 100644 --- a/SecretAPI/Attributes/CallOnLoadAttribute.cs +++ b/SecretAPI/Attributes/CallOnLoadAttribute.cs @@ -16,9 +16,11 @@ public class CallOnLoadAttribute : Attribute, IPriority /// Initializes a new instance of the class. /// /// The priority of the load. - public CallOnLoadAttribute(int priority = 0) + /// Whether it should source generate the method call. False will make it slower during runtime. + public CallOnLoadAttribute(int priority = 0, bool shouldSourceGen = true) { Priority = priority; + ShouldSourceGen = shouldSourceGen; } /// @@ -26,6 +28,12 @@ public CallOnLoadAttribute(int priority = 0) /// public int Priority { get; } + /// + /// Gets a value indicating whether it should source generate the method call. + /// + /// If disabled, this will do runtime reflection which is slower. + public bool ShouldSourceGen { get; } + /// /// Loads and calls all . /// diff --git a/SecretAPI/Features/Commands/Attributes/CommandSenderAttribute.cs b/SecretAPI/Features/Commands/Attributes/CommandSenderAttribute.cs new file mode 100644 index 0000000..5413eea --- /dev/null +++ b/SecretAPI/Features/Commands/Attributes/CommandSenderAttribute.cs @@ -0,0 +1,15 @@ +namespace SecretAPI.Features.Commands.Attributes +{ + using System; + using CommandSystem; + using LabApi.Features.Wrappers; + + /// + /// Defines a parameter as accepting the command sender. + /// + /// This must be , or . + [AttributeUsage(AttributeTargets.Parameter)] + public class CommandSenderAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Attributes/ExecuteCommandAttribute.cs b/SecretAPI/Features/Commands/Attributes/ExecuteCommandAttribute.cs new file mode 100644 index 0000000..0efecd2 --- /dev/null +++ b/SecretAPI/Features/Commands/Attributes/ExecuteCommandAttribute.cs @@ -0,0 +1,12 @@ +namespace SecretAPI.Features.Commands.Attributes +{ + using System; + + /// + /// Attribute used to identify a method as a possible execution result. + /// + [AttributeUsage(AttributeTargets.Method)] + public class ExecuteCommandAttribute : Attribute + { + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Attributes/ValidateArgumentAttribute.cs b/SecretAPI/Features/Commands/Attributes/ValidateArgumentAttribute.cs new file mode 100644 index 0000000..5a1e89c --- /dev/null +++ b/SecretAPI/Features/Commands/Attributes/ValidateArgumentAttribute.cs @@ -0,0 +1,26 @@ +namespace SecretAPI.Features.Commands.Attributes +{ + using System; + using SecretAPI.Features.Commands.Validators; + + /// + /// Defines the attribute needed to auto validate with . + /// + [AttributeUsage(AttributeTargets.Parameter)] + public class ValidateArgumentAttribute : Attribute + { + /// + /// Initializes a new instance of the class. + /// + /// The of the . + public ValidateArgumentAttribute(Type type) + { + Type = type; + } + + /// + /// Gets the of the . + /// + public Type Type { get; } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/CommandResult.cs b/SecretAPI/Features/Commands/CommandResult.cs new file mode 100644 index 0000000..efcb09c --- /dev/null +++ b/SecretAPI/Features/Commands/CommandResult.cs @@ -0,0 +1,18 @@ +namespace SecretAPI.Features.Commands +{ + /// + /// Defines the result of a . + /// + public readonly struct CommandResult(bool success, string response) + { + /// + /// Whether the command succeeded. + /// + public readonly bool Success = success; + + /// + /// The response to give after command use. + /// + public readonly string Response = response; + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/CustomCommand.cs b/SecretAPI/Features/Commands/CustomCommand.cs new file mode 100644 index 0000000..158c67b --- /dev/null +++ b/SecretAPI/Features/Commands/CustomCommand.cs @@ -0,0 +1,32 @@ +namespace SecretAPI.Features.Commands +{ + using System; + using CommandSystem; + + /// + /// Defines the base of a custom . + /// + public abstract partial class CustomCommand : ICommand + { + /// + public abstract string Command { get; } + + /// + public abstract string Description { get; } + + /// + public virtual string[] Aliases { get; } = []; + + /// + /// Gets an array of the sub commands for this command. + /// + public virtual CustomCommand[] SubCommands { get; } = []; + + /// + /// This should not be overwritten except by source generation. + public virtual bool Execute(ArraySegment arguments, ICommandSender sender, out string response) + { + throw new NotImplementedException($"Command {Command} not implemented. Did source generation fail? - If this is not intentional, submit a bugreport!"); + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Validators/CommandValidationResult.cs b/SecretAPI/Features/Commands/Validators/CommandValidationResult.cs new file mode 100644 index 0000000..ed6b612 --- /dev/null +++ b/SecretAPI/Features/Commands/Validators/CommandValidationResult.cs @@ -0,0 +1,44 @@ +namespace SecretAPI.Features.Commands.Validators +{ + /// + /// Defines the result of a . + /// + /// The type this is validating. + public readonly struct CommandValidationResult + { + /// + /// Whether the validation was successful. + /// + public readonly bool Success; + + /// + /// Gets the value when successful. + /// + public readonly T? Value; + + /// + /// The error message, if any exists. + /// + public readonly string? ErrorMessage; + + /// + /// Initializes a new instance of the struct. + /// + /// The value that was validated. + public CommandValidationResult(T value) + { + Value = value; + Success = true; + } + + /// + /// Initializes a new instance of the struct. + /// + /// The error message, including how it went wrong. + public CommandValidationResult(string error) + { + ErrorMessage = error; + Success = false; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Validators/EnumArgumentValidator.cs b/SecretAPI/Features/Commands/Validators/EnumArgumentValidator.cs new file mode 100644 index 0000000..979f8e8 --- /dev/null +++ b/SecretAPI/Features/Commands/Validators/EnumArgumentValidator.cs @@ -0,0 +1,20 @@ +namespace SecretAPI.Features.Commands.Validators +{ + using System; + + /// + /// Validator for . + /// + /// The to validate. + public sealed class EnumArgumentValidator : ICommandArgumentValidator + where TEnum : struct, Enum + { + /// + public CommandValidationResult Validate(string argument) + { + return Enum.TryParse(argument, true, out TEnum value) + ? new CommandValidationResult(value) + : new CommandValidationResult($"Argument provided was not a valid {typeof(TEnum).Name}"); + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Validators/ICommandArgumentValidator.cs b/SecretAPI/Features/Commands/Validators/ICommandArgumentValidator.cs new file mode 100644 index 0000000..d3b39a0 --- /dev/null +++ b/SecretAPI/Features/Commands/Validators/ICommandArgumentValidator.cs @@ -0,0 +1,21 @@ +namespace SecretAPI.Features.Commands.Validators +{ + /// + /// Defines the base of a validator for . + /// + public interface ICommandArgumentValidator; + + /// + /// Defines the base of a validator for . + /// + /// The type this validator is for. + public interface ICommandArgumentValidator : ICommandArgumentValidator + { + /// + /// Validates the specified argument. + /// + /// The argument needed to validate. + /// The result of the validation. + public CommandValidationResult Validate(string argument); + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Validators/PlayerArgumentValidator.cs b/SecretAPI/Features/Commands/Validators/PlayerArgumentValidator.cs new file mode 100644 index 0000000..f79547b --- /dev/null +++ b/SecretAPI/Features/Commands/Validators/PlayerArgumentValidator.cs @@ -0,0 +1,30 @@ +namespace SecretAPI.Features.Commands.Validators +{ + using LabApi.Features.Wrappers; + + /// + /// Validates command argument for . + /// + public sealed class PlayerArgumentValidator : ICommandArgumentValidator + { + /// + public CommandValidationResult Validate(string argument) + { + // player id + if (int.TryParse(argument, out int value) && Player.TryGet(value, out Player? found)) + return new CommandValidationResult(found); + + // player user id + if (Player.TryGet(argument, out found)) + return new CommandValidationResult(found); + + foreach (Player player in Player.List) + { + if (player.Nickname == argument || player.UserId == argument) + return new CommandValidationResult(player); + } + + return new CommandValidationResult($"{argument} is not a valid player!"); + } + } +} \ No newline at end of file diff --git a/SecretAPI/Features/Commands/Validators/ValidatorSingleton.cs b/SecretAPI/Features/Commands/Validators/ValidatorSingleton.cs new file mode 100644 index 0000000..5c1e1e7 --- /dev/null +++ b/SecretAPI/Features/Commands/Validators/ValidatorSingleton.cs @@ -0,0 +1,15 @@ +namespace SecretAPI.Features.Commands.Validators +{ + /// + /// Handles singleton-ing . + /// + /// The type. + public static class ValidatorSingleton + where T : class, ICommandArgumentValidator, new() + { + /// + /// The current static instance. + /// + public static readonly T Instance = new(); + } +} \ No newline at end of file diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index c4335e7..7ba6670 100644 --- a/SecretAPI/SecretAPI.csproj +++ b/SecretAPI/SecretAPI.csproj @@ -4,18 +4,18 @@ net48 latest enable - 2.0.3 + 2.0.4 true - + true true - Misfiy + obvEve SecretAPI API to extend SCP:SL LabAPI git - https://github.com/Misfiy/SecretAPI + https://github.com/obvEve/SecretAPI README.md MIT @@ -25,13 +25,18 @@ True \ + + True + analyzers/dotnet/cs + + + + + - diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 72634a7..367d34a 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -6,12 +6,11 @@ using LabApi.Features; using LabApi.Loader.Features.Plugins; using LabApi.Loader.Features.Plugins.Enums; - using SecretAPI.Attributes; /// /// Main class handling loading API. /// - public class SecretApi : Plugin + public partial class SecretApi : Plugin { /// public override string Name => "SecretAPI"; @@ -49,7 +48,7 @@ public class SecretApi : Plugin public override void Enable() { Harmony = new Harmony("SecretAPI" + DateTime.Now); - CallOnLoadAttribute.Load(Assembly); + OnLoad(); } ///