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