diff --git a/.github/workflows/nuget.yml b/.github/workflows/nuget.yml index 64077c7..abe8ebc 100644 --- a/.github/workflows/nuget.yml +++ b/.github/workflows/nuget.yml @@ -11,6 +11,7 @@ jobs: env: REFERENCES_URL: https://exmod-team.github.io/SL-References/Dev.zip REFERENCES_PATH: ${{ github.workspace }}/References + NUGET_PACKAGED_PATH: ${{ github.workspace }}/nupkgs steps: - name: Checkout @@ -28,9 +29,9 @@ jobs: - name: Build and Pack NuGet env: SL_REFERENCES: ${{ env.REFERENCES_PATH }} - run: dotnet pack -c Release --output ${GITHUB_WORKSPACE}/nupkgs + run: dotnet pack -c Release --output ${NUGET_PACKAGED_PATH} - name: Push NuGet package run: | - $PackageFile = (Get-ChildItem -Path "${GITHUB_WORKSPACE}/nupkgs" -Include 'SecretAPI.*.nupkg' -Recurse | Select-Object -First 1).FullName + $PackageFile = (Get-ChildItem -Path "${NUGET_PACKAGED_PATH}" -Include 'SecretAPI.*.nupkg' -Recurse | Select-Object -First 1).FullName dotnet nuget push $PackageFile --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 9996c17..1af2023 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -30,12 +30,11 @@ jobs: env: SL_REFERENCES: ${{ env.REFERENCES_PATH }} shell: pwsh - run: | - dotnet build -c Release + run: dotnet build -c Release - name: Upload uses: actions/upload-artifact@v4 with: name: Build Result - path: ${{ github.workspace }}\SecretAPI\bin\Release\net48\SecretAPI.dll - retention-days: 7 + path: ${{ github.workspace }}/**/bin/Release/net48/*SecretAPI*.dll + retention-days: 7 \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/CallOnLoadGenerator.cs b/SecretAPI.CodeGeneration/CallOnLoadGenerator.cs new file mode 100644 index 0000000..12580f9 --- /dev/null +++ b/SecretAPI.CodeGeneration/CallOnLoadGenerator.cs @@ -0,0 +1,106 @@ +namespace SecretAPI.CodeGeneration; + +/// +/// Code generator for CallOnLoad/CallOnUnload +/// +[Generator] +public class CallOnLoadGenerator : IIncrementalGenerator +{ + private const string PluginBaseClassName = "Plugin"; + private const string CallOnLoadAttributeLocation = "SecretAPI.Attribute.CallOnLoadAttribute"; + private const string CallOnUnloadAttributeLocation = "SecretAPI.Attribute.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 is { IsAbstract: false, BaseType.Name: PluginBaseClassName })!; + + 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 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) + .Select(m => m.method) + .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation)) + .ToArray(); + + IMethodSymbol[] unloadCalls = methods + .Where(m => m.isUnload) + .Select(m => m.method) + .OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation)) + .ToArray(); + + CompilationUnitSyntax compilation = ClassBuilder.CreateBuilder(pluginClassSymbol) + .AddUsingStatements("System") + .AddModifiers(SyntaxKind.PartialKeyword) + .StartMethodCreation("OnLoad", "void") + .AddModifiers(SyntaxKind.PublicKeyword) + .AddStatements(MethodCallStatements(loadCalls)) + .FinishMethodBuild() + .StartMethodCreation("OnUnload", "void") + .AddModifiers(SyntaxKind.PublicKeyword) + .AddStatements(MethodCallStatements(unloadCalls)) + .FinishMethodBuild() + .Build(); + + context.AddSource($"{pluginClassSymbol.Name}.g.cs", compilation.ToFullString()); + } +} \ 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..80b187d --- /dev/null +++ b/SecretAPI.CodeGeneration/CodeBuilders/ClassBuilder.cs @@ -0,0 +1,64 @@ +namespace SecretAPI.CodeGeneration.CodeBuilders; + +internal class ClassBuilder +{ + private NamespaceDeclarationSyntax _namespaceDeclaration; + private ClassDeclarationSyntax _classDeclaration; + private string _className; + + private readonly List _modifiers = new(); + private readonly List _usings = new(); + private readonly List _methods = new(); + + private ClassBuilder(string @namespace, string className) + { + _namespaceDeclaration = NamespaceDeclaration(ParseName(@namespace)); + _className = className; + _classDeclaration = ClassDeclaration(className); + + AddUsingStatements("System.CodeDom.Compiler"); + } + + internal static ClassBuilder CreateBuilder(string @namespace, string className) + => new(@namespace, className); + + internal static ClassBuilder CreateBuilder(INamedTypeSymbol namedClass) + => new(namedClass.ContainingNamespace.ToDisplayString(), namedClass.Name); + + internal ClassBuilder AddUsingStatements(params string[] usingStatement) + { + foreach (string statement in usingStatement) + _usings.Add(UsingDirective(ParseName(statement))); + + return this; + } + + internal MethodBuilder StartMethodCreation(string methodName, string returnType) => new(this, methodName, returnType); + + internal void AddMethodDefinition(MethodDeclarationSyntax method) => _methods.Add(method); + + internal ClassBuilder AddModifiers(params SyntaxKind[] modifiers) + { + foreach (SyntaxKind token in modifiers) + _modifiers.Add(Token(token)); + + return this; + } + + 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); + } +} \ 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..f3fb0ed --- /dev/null +++ b/SecretAPI.CodeGeneration/CodeBuilders/MethodBuilder.cs @@ -0,0 +1,53 @@ +namespace SecretAPI.CodeGeneration.CodeBuilders; + +internal class MethodBuilder +{ + private readonly ClassBuilder _classBuilder; + private readonly List _modifiers = new(); + private readonly List _parameters = new(); + private readonly List _statements = new(); + private readonly string _methodName; + private readonly string _returnType; + + internal MethodBuilder(ClassBuilder classBuilder, string methodName, string 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 MethodBuilder AddModifiers(params SyntaxKind[] modifiers) + { + foreach (SyntaxKind token in modifiers) + _modifiers.Add(Token(token)); + + return this; + } + + internal ClassBuilder FinishMethodBuild() + { + BlockSyntax body = _statements.Any() ? Block(_statements) : Block(); + + MethodDeclarationSyntax methodDeclaration = MethodDeclaration(ParseTypeName(_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/CustomCommandGenerator.cs b/SecretAPI.CodeGeneration/CustomCommandGenerator.cs new file mode 100644 index 0000000..c1038f2 --- /dev/null +++ b/SecretAPI.CodeGeneration/CustomCommandGenerator.cs @@ -0,0 +1,103 @@ +namespace SecretAPI.CodeGeneration; + +/// +/// 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 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 ctx, + INamedTypeSymbol namedClassSymbol, + ImmutableArray executeMethods) + { + if (namedClassSymbol.IsAbstract) + return; + + if (namedClassSymbol.BaseType?.Name != CommandName) + return; + + CompilationUnitSyntax compilation = ClassBuilder.CreateBuilder(namedClassSymbol) + .AddUsingStatements("System", "System.Collections.Generic") + .AddUsingStatements("CommandSystem") + .AddModifiers(SyntaxKind.PartialKeyword) + .StartMethodCreation(ExecuteMethodName, "bool") + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .AddParameters(ArgumentsParam, SenderParam, ResponseParam) + .FinishMethodBuild() + .Build(); + + ctx.AddSource($"{namedClassSymbol.Name}.g.cs", compilation.ToFullString()); + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/GlobalUsings.cs b/SecretAPI.CodeGeneration/GlobalUsings.cs new file mode 100644 index 0000000..6492204 --- /dev/null +++ b/SecretAPI.CodeGeneration/GlobalUsings.cs @@ -0,0 +1,10 @@ +global using Microsoft.CodeAnalysis; +global using Microsoft.CodeAnalysis.CSharp; +global using Microsoft.CodeAnalysis.CSharp.Syntax; +global using System.Collections.Immutable; +global using SecretAPI.CodeGeneration.CodeBuilders; +global using SecretAPI.CodeGeneration.Utils; + +global using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; +global using static Microsoft.CodeAnalysis.CSharp.SyntaxFacts; +global using static SecretAPI.CodeGeneration.Utils.Util; \ 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..f4f21ff --- /dev/null +++ b/SecretAPI.CodeGeneration/SecretAPI.CodeGeneration.csproj @@ -0,0 +1,21 @@ + + + + netstandard2.0 + 10 + enable + enable + + + + true + false + Analyzer + + + + + + + + diff --git a/SecretAPI.CodeGeneration/Utils/MethodParameter.cs b/SecretAPI.CodeGeneration/Utils/MethodParameter.cs new file mode 100644 index 0000000..be3df2d --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/MethodParameter.cs @@ -0,0 +1,46 @@ +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: _attributeLists, + modifiers: _modifiers, + type: _type, + identifier: _identifier, + @default: _default); +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/Util.cs b/SecretAPI.CodeGeneration/Utils/Util.cs new file mode 100644 index 0000000..71e164f --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/Util.cs @@ -0,0 +1,47 @@ +namespace SecretAPI.CodeGeneration.Utils; + +public static class Util +{ + 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())); + + public static TypeSyntax GetSingleGenericTypeSyntax(string genericName, SyntaxKind predefinedType) + => GenericName(genericName) + .WithTypeArgumentList( + TypeArgumentList( + SingletonSeparatedList( + PredefinedType( + Token(predefinedType))))); + + public static PredefinedTypeSyntax GetPredefinedTypeSyntax(SyntaxKind kind) + => PredefinedType(Token(kind)); + + public static StatementSyntax MethodCallStatement(string typeName, string methodName) + => ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + ParseTypeName(typeName), IdentifierName(methodName)))); + + public static StatementSyntax[] MethodCallStatements(IMethodSymbol[] methodCalls) + { + List statements = new(); + + foreach (IMethodSymbol methodCall in methodCalls) + statements.Add(MethodCallStatement(methodCall.ContainingType.ToDisplayString(), methodCall.Name)); + + return statements.ToArray(); + } +} \ No newline at end of file diff --git a/SecretAPI.CodeGeneration/Utils/WritingUtils.cs b/SecretAPI.CodeGeneration/Utils/WritingUtils.cs new file mode 100644 index 0000000..113a8ba --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/WritingUtils.cs @@ -0,0 +1,118 @@ +/*namespace SecretAPI.CodeGeneration.Utils; + +using System.CodeDom.Compiler; +using Microsoft.CodeAnalysis; + +public static class WritingUtils +{ + public static string GetAccessibilityString(this Accessibility accessibility) + { + return accessibility switch + { + Accessibility.Private => "private", + Accessibility.ProtectedAndInternal => "protected internal", + Accessibility.Protected => "protected", + Accessibility.Internal => "internal", + Accessibility.Public => "public", + _ => throw new ArgumentOutOfRangeException(nameof(accessibility), accessibility, "Accessibility not supported") + }; + } + + public static IndentedTextWriter WriteGeneratedText(this IndentedTextWriter writer) + { + writer.WriteLine("// "); + writer.WriteLine($"// Generated {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); + writer.WriteLine(); + return writer; + } + + public static IndentedTextWriter WriteNamespace(this IndentedTextWriter writer, INamedTypeSymbol symbol, bool startBrace) + { + if (symbol.ContainingNamespace == null) + return writer; + + writer.WriteLine($"namespace {symbol.ContainingNamespace}"); + if (startBrace) + WriteStartBrace(writer); + + return writer; + } + + public static IndentedTextWriter WriteUsings(this IndentedTextWriter writer, params string[] usings) + { + if (usings.Length == 0) + return writer; + + foreach (string @using in usings) + writer.WriteLine($"using {@using};"); + + writer.WriteLine(); + return writer; + } + + public static IndentedTextWriter WritePartialClass(this IndentedTextWriter writer, string className, bool startBrace) + { + writer.WriteLine($"partial class {className}"); + if (startBrace) + { + writer.WriteLine("{"); + writer.Indent++; + } + + return writer; + } + + public static IndentedTextWriter WriteMethod( + this IndentedTextWriter writer, + string methodName, + string returnType, + bool isOverride, + Accessibility accessibility, + bool startBrace, + params string[] parameters) + { + writer.Write(GetAccessibilityString(accessibility)); + if (isOverride) + writer.Write(" override "); + writer.Write(returnType); + writer.Write(" " + methodName); + writer.WriteLine("("); + writer.Indent++; + + for (int index = 0; index < parameters.Length; index++) + { + string parameter = parameters[index]; + if (parameters.Length > index + 1) + writer.WriteLine(parameter + ","); + else if (!startBrace) + writer.Write(parameter + ")"); + else + writer.WriteLine(parameter + ")"); + } + + writer.Indent--; + + if (startBrace) + writer.WriteStartBrace(); + + return writer; + } + + public static IndentedTextWriter WriteStartBrace(this IndentedTextWriter writer) + { + writer.WriteLine("{"); + writer.Indent++; + return writer; + } + + public static IndentedTextWriter FinishAllIndentations(this IndentedTextWriter writer) + { + while (writer.Indent > 0) + { + writer.Indent--; + writer.WriteLine("}"); + } + + return writer; + } +}*/ \ 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..3ce72ec --- /dev/null +++ b/SecretAPI.Examples/Commands/ExampleExplodeCommand.cs @@ -0,0 +1,26 @@ +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 void 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); + } + } +} \ 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..98232ae --- /dev/null +++ b/SecretAPI.Examples/Commands/ExampleParentCommand.cs @@ -0,0 +1,32 @@ +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 void Run([CommandSender] Player sender) + { + Logger.Debug($"Example parent was run by {sender.Nickname}"); + } + } +} \ 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/Features/Commands/Attributes/CommandSenderAttribute.cs b/SecretAPI/Features/Commands/Attributes/CommandSenderAttribute.cs new file mode 100644 index 0000000..cb85204 --- /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/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/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index ba3d4b3..b8b3eff 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,6 +25,14 @@ True \ + + True + analyzers/dotnet/cs + + + + + diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 6272099..73db453 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -11,7 +11,7 @@ /// /// Main class handling loading API. /// - public class SecretApi : Plugin + public partial class SecretApi : Plugin { /// public override string Name => "SecretAPI"; @@ -49,7 +49,7 @@ public class SecretApi : Plugin public override void Enable() { Harmony = new Harmony("SecretAPI" + DateTime.Now); - CallOnLoadAttribute.Load(Assembly); + OnLoad(); } ///