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..417ff8f --- /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, Comment("#nullable enable"), 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/CallLoadDiagnostics.cs b/SecretAPI.CodeGeneration/Diagnostics/CallLoadDiagnostics.cs new file mode 100644 index 0000000..36e35f8 --- /dev/null +++ b/SecretAPI.CodeGeneration/Diagnostics/CallLoadDiagnostics.cs @@ -0,0 +1,12 @@ +namespace SecretAPI.CodeGeneration.Diagnostics; + +internal static class CallLoadDiagnostics +{ + internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new( + "SecretGen1", + "Plugin class must be partial", + "Plugin class '{0}' is missing partial modifier", + "Usage", + DiagnosticSeverity.Error, + true); +} \ 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..edf2995 --- /dev/null +++ b/SecretAPI.CodeGeneration/Generators/CallOnLoadGenerator.cs @@ -0,0 +1,133 @@ +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<(ClassDeclarationSyntax?, INamedTypeSymbol?)> pluginClassProvider = + context.SyntaxProvider.CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax, + static (ctx, _) => ( + ctx.Node as ClassDeclarationSyntax, ctx.SemanticModel.GetDeclaredSymbol(ctx.Node) as INamedTypeSymbol)) + .Where(static c => c.Item2 != null && !c.Item2.IsAbstract && c.Item2.BaseType?.Name == PluginBaseClassName && + c.Item2.BaseType.ContainingNamespace.ToDisplayString() == PluginNamespace); + + context.RegisterSourceOutput(pluginClassProvider.Combine(callProvider.Collect()), static (context, data) => + { + Generate(context, new Tuple(data.Left.Item1, data.Left.Item2), 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, + Tuple pluginInfo, + ImmutableArray<(IMethodSymbol method, bool isLoad, bool isUnload)> methods) + { + if (pluginInfo.Item1 == null || pluginInfo.Item2 == null || methods.IsEmpty) + return; + + if (!pluginInfo.Item1.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + context.ReportDiagnostic( + Diagnostic.Create( + CallLoadDiagnostics.MustBePartialPluginClass, + pluginInfo.Item1.GetLocation(), + pluginInfo.Item1.Identifier.Text + ) + ); + } + + 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(pluginInfo.Item2) + .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, $"{pluginInfo.Item2.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..e27d33d --- /dev/null +++ b/SecretAPI.CodeGeneration/Generators/CustomCommandGenerator.cs @@ -0,0 +1,232 @@ +namespace SecretAPI.CodeGeneration.Generators; + +using System.Linq.Expressions; + +/// +/// 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 const string ArgumentsParamName = "arguments"; + private const string SenderParamName = "sender"; + private const string ResponseParamName = "response"; + + private static readonly MethodParameter ArgumentsParam = + new( + identifier: ArgumentsParamName, + type: GetSingleGenericTypeSyntax("ArraySegment", SyntaxKind.StringKeyword) + ); + + private static readonly MethodParameter SenderParam = + new( + identifier: SenderParamName, + type: IdentifierName("ICommandSender") + ); + + private static readonly MethodParameter ResponseParam = + new( + identifier: ResponseParamName, + 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) + { + const string ResultArgName = "result"; + + if (namedClassSymbol.IsAbstract) + return; + + if (namedClassSymbol.BaseType?.Name != CommandName) + return; + + ClassBuilder classBuilder = ClassBuilder.CreateBuilder(namedClassSymbol) + .AddUsingStatements("System", "System.Linq", "System.Collections.Generic") + .AddUsingStatements("CommandSystem") + .AddUsingStatements("SecretAPI.Features.Commands", "SecretAPI.Features.Commands.Validators") + .AddModifiers(SyntaxKind.PartialKeyword); + + List executeValidateStatements = new(); + 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 + ) + ); + + continue; + } + + executeValidateStatements.Add(GetExecuteCheckSyntax(method)); + } + + LocalDeclarationStatementSyntax resultDeclaration = LocalDeclarationStatement( + VariableDeclaration(NullableType(IdentifierName(CommandResultLocation))) + .AddVariables(VariableDeclarator(ResultArgName) + .WithInitializer(EqualsValueClause(LiteralExpression(SyntaxKind.NullLiteralExpression))))); + + classBuilder.StartMethodCreation(ExecuteMethodName, SyntaxKind.BoolKeyword) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.OverrideKeyword) + .AddParameters(ArgumentsParam, SenderParam, ResponseParam) + .AddStatements(GenerateSubCommandCheck()) + .AddStatements(resultDeclaration) + .AddStatements(executeValidateStatements.ToArray()) + /*.AddStatements(ReturnStatement(LiteralExpression(SyntaxKind.TrueLiteralExpression)))*/ + .FinishMethodBuild(); + + classBuilder.Build(context, $"{namedClassSymbol.Name}.g.cs"); + } + + private static StatementSyntax GetExecuteCheckSyntax(MethodDeclarationSyntax methodDeclarationSyntax) + { + List statements = new(); + + foreach (ParameterSyntax parameterSyntax in methodDeclarationSyntax.ParameterList.Parameters) + { + // The CommandSenderAttribute if exists - defines that the param is intended for the sender and is not required + AttributeSyntax? commandSenderAttribute = parameterSyntax.AttributeLists + .SelectMany(list => list.Attributes) + .FirstOrDefault(attribute => attribute.Name.ToString() == "CommandSenderAttribute"); + + // The ValidateArgumentAttribute if exists - contains the custom Type for the validator the user wants on the param + AttributeSyntax? validateAttribute = parameterSyntax.AttributeLists + .SelectMany(list => list.Attributes) + .FirstOrDefault(attribute => attribute.Name.ToString() == "ValidateArgumentAttribute"); + + bool isOptional = parameterSyntax.Default != null; // whether the param is optional and thus does not require user input + ExpressionSyntax? defaultValue = parameterSyntax.Default?.Value; // the default value expression when optional, otherwise null + + TypeOfExpressionSyntax? validateAttributeType = validateAttribute?.ArgumentList?.Arguments.First().Expression as TypeOfExpressionSyntax; + } + + return Block(); + } + + private static StatementSyntax GenerateSubCommandCheck() + { + const string CheckSubCommandMethodIdentifier = "CheckSubCommand"; + const string CheckSubCommandCommandParamIdentifier = "subCommand"; + const string CheckSubCommandResultIdentifier = "checkSubCommandResult"; + + // bool checkSubCommandResult = subCommand.Execute(arguments, sender, out response); + LocalDeclarationStatementSyntax subCommandExecute = LocalDeclarationStatement( + VariableDeclaration(GetPredefinedTypeSyntax(SyntaxKind.BoolKeyword)) + .AddVariables(VariableDeclarator(CheckSubCommandResultIdentifier) + .WithInitializer(EqualsValueClause( + InvocationExpression(MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(CheckSubCommandCommandParamIdentifier), + IdentifierName(ExecuteMethodName))).WithArgumentList(ArgumentList(SeparatedList(new[] + { + Argument(IdentifierName(ArgumentsParamName)), + Argument(IdentifierName(SenderParamName)), + Argument(IdentifierName(ResponseParamName)) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword)) + }))))))); + + // return checkSubCommandResult; + ReturnStatementSyntax returnStatement = ReturnStatement(IdentifierName(CheckSubCommandResultIdentifier)); + + // if (CheckSubCommand(arguments.First(), out CustomCommand? subCommand)) + IfStatementSyntax ifSubCommandCheck = IfStatement( + InvocationExpression(IdentifierName(CheckSubCommandMethodIdentifier)) + .WithArgumentList(ArgumentList(SeparatedList(new[] + { + Argument( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(ArgumentsParamName), + IdentifierName("First")))), + Argument( + DeclarationExpression( + NullableType( + IdentifierName(CommandName)), + SingleVariableDesignation( + Identifier(CheckSubCommandCommandParamIdentifier)))) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword)) + }))), + Block(subCommandExecute, returnStatement)); + + // if (arguments.Any()) + IfStatementSyntax ifArgumentsAnyStatement = IfStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName(ArgumentsParamName), + IdentifierName("Any"))), + Block(ifSubCommandCheck)); + + return ifArgumentsAnyStatement; + } +} + +/* + +ValidatorSingleton.Instance.Validate(arguments.First()); +ValidatorSingleton>.Instance.Validate(arguments.First()); + +*/ \ 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..776280d --- /dev/null +++ b/SecretAPI.CodeGeneration/Utils/TypeUtils.cs @@ -0,0 +1,10 @@ +namespace SecretAPI.CodeGeneration.Utils; + +internal static class TypeUtils +{ + internal static PredefinedTypeSyntax GetPredefinedTypeSyntax(SyntaxKind kind) + => PredefinedType(Token(kind)); + + internal static TypeSyntax GetTypeSyntax(string typeIdentifier) + => IdentifierName(typeIdentifier); +} \ 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..df07462 --- /dev/null +++ b/SecretAPI.Examples/Commands/ExampleExplodeCommand.cs @@ -0,0 +1,29 @@ +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 = null) + { + target ??= sender; + + 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/ExampleEntry.cs b/SecretAPI.Examples/ExampleEntry.cs index 7381b08..32ee205 100644 --- a/SecretAPI.Examples/ExampleEntry.cs +++ b/SecretAPI.Examples/ExampleEntry.cs @@ -5,7 +5,6 @@ using HarmonyLib; using LabApi.Loader.Features.Plugins; using SecretAPI.Examples.Settings; - using SecretAPI.Extensions; using SecretAPI.Features.UserSettings; /// diff --git a/SecretAPI.Examples/Patches/ExamplePatch.cs b/SecretAPI.Examples/Patches/ExamplePatch.cs index fde590e..167939b 100644 --- a/SecretAPI.Examples/Patches/ExamplePatch.cs +++ b/SecretAPI.Examples/Patches/ExamplePatch.cs @@ -1,6 +1,6 @@ namespace SecretAPI.Examples.Patches { - using SecretAPI.Attribute; + using SecretAPI.Attributes; /// /// An example harmony patch. 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/Attribute/CallOnLoadAttribute.cs b/SecretAPI/Attributes/CallOnLoadAttribute.cs similarity index 82% rename from SecretAPI/Attribute/CallOnLoadAttribute.cs rename to SecretAPI/Attributes/CallOnLoadAttribute.cs index cb55356..903df53 100644 --- a/SecretAPI/Attribute/CallOnLoadAttribute.cs +++ b/SecretAPI/Attributes/CallOnLoadAttribute.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Attribute +namespace SecretAPI.Attributes { using System; using System.Collections.Generic; @@ -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/Attribute/CallOnUnloadAttribute.cs b/SecretAPI/Attributes/CallOnUnloadAttribute.cs similarity index 97% rename from SecretAPI/Attribute/CallOnUnloadAttribute.cs rename to SecretAPI/Attributes/CallOnUnloadAttribute.cs index b886d55..65da120 100644 --- a/SecretAPI/Attribute/CallOnUnloadAttribute.cs +++ b/SecretAPI/Attributes/CallOnUnloadAttribute.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Attribute +namespace SecretAPI.Attributes { using System; using System.Reflection; diff --git a/SecretAPI/Attribute/HarmonyPatchCategory.cs b/SecretAPI/Attributes/HarmonyPatchCategory.cs similarity index 95% rename from SecretAPI/Attribute/HarmonyPatchCategory.cs rename to SecretAPI/Attributes/HarmonyPatchCategory.cs index a27b953..853129d 100644 --- a/SecretAPI/Attribute/HarmonyPatchCategory.cs +++ b/SecretAPI/Attributes/HarmonyPatchCategory.cs @@ -1,4 +1,4 @@ -namespace SecretAPI.Attribute +namespace SecretAPI.Attributes { using System; using SecretAPI.Extensions; diff --git a/SecretAPI/Extensions/CollectionExtensions.cs b/SecretAPI/Extensions/CollectionExtensions.cs index 28e1887..83af57e 100644 --- a/SecretAPI/Extensions/CollectionExtensions.cs +++ b/SecretAPI/Extensions/CollectionExtensions.cs @@ -11,37 +11,38 @@ /// public static class CollectionExtensions { - /// - /// Gets a random value from the collection. - /// /// The collection to pull from. /// The Type contained by the collection. - /// A random value, default value when empty collection. - /// Will occur if the collection is empty. - public static T GetRandomValue(this IEnumerable collection) + extension(IEnumerable collection) { - TryGetRandomValue(collection, out T? value); - return value!; - } - - /// - /// Tries to get a random value from . - /// - /// The to try and get a random value from. - /// The value that was found. Default if none could be found. - /// The type contained within the . - /// Whether a non-null value was found. - public static bool TryGetRandomValue(this IEnumerable collection, [NotNullWhen(true)] out T? value) - { - IList list = collection as IList ?? collection.ToList(); - if (list.Count == 0) + /// + /// Gets a random value from the collection. + /// + /// A random value, default value when empty collection. + /// Will occur if the collection is empty. + public T GetRandomValue() { - value = default; - return false; + TryGetRandomValue(collection, out T? value); + return value!; } - value = list[Random.Range(0, list.Count)]; - return value != null; + /// + /// Tries to get a random value from . + /// + /// The value that was found. Default if none could be found. + /// Whether a non-null value was found. + public bool TryGetRandomValue([NotNullWhen(true)] out T? value) + { + IList list = collection as IList ?? collection.ToList(); + if (list.Count == 0) + { + value = default; + return false; + } + + value = list[Random.Range(0, list.Count)]; + return value != null; + } } } } \ No newline at end of file diff --git a/SecretAPI/Extensions/HarmonyExtensions.cs b/SecretAPI/Extensions/HarmonyExtensions.cs index 2f623b1..f26b436 100644 --- a/SecretAPI/Extensions/HarmonyExtensions.cs +++ b/SecretAPI/Extensions/HarmonyExtensions.cs @@ -6,62 +6,63 @@ using System.Reflection; using HarmonyLib; using LabApi.Features.Console; - using SecretAPI.Attribute; + using SecretAPI.Attributes; /// /// Handles patching. /// public static class HarmonyExtensions { - /// - /// Patches all methods with the proper . - /// /// The harmony to use for the patch. - /// The category to patch. - /// The assembly to find patches in. - public static void PatchCategory(this Harmony harmony, string category, Assembly? assembly = null) + extension(Harmony harmony) { - assembly ??= Assembly.GetCallingAssembly(); - - assembly.GetTypes().Where(type => - { - IEnumerable categories = type.GetCustomAttributes(); - return categories.Any(c => c.Category == category); - }) - .Do(type => SafePatch(harmony, type)); - } - - /// - /// Patches all patches that don't have a . - /// - /// The harmony to use for the patch. - /// The assembly to look for patches. - public static void PatchAllNoCategory(this Harmony harmony, Assembly? assembly = null) - { - assembly ??= Assembly.GetCallingAssembly(); + /// + /// Patches all methods with the proper . + /// + /// The category to patch. + /// The assembly to find patches in. + public void PatchCategory(string category, Assembly? assembly = null) + { + assembly ??= Assembly.GetCallingAssembly(); - assembly.GetTypes().Where(type => - { - IEnumerable categories = type.GetCustomAttributes(); - return !categories.Any(); - }) - .Do(type => SafePatch(harmony, type)); - } + assembly.GetTypes().Where(type => + { + IEnumerable categories = type.GetCustomAttributes(); + return categories.Any(c => c.Category == category); + }) + .Do(type => SafePatch(harmony, type)); + } - /// - /// Attempts to safely patch a , logging any errors. - /// - /// The harmony to use for the patch. - /// The to attempt to patch. - public static void SafePatch(this Harmony harmony, Type type) - { - try + /// + /// Patches all patches that don't have a . + /// + /// The assembly to look for patches. + public void PatchAllNoCategory(Assembly? assembly = null) { - harmony.CreateClassProcessor(type).Patch(); + assembly ??= Assembly.GetCallingAssembly(); + + assembly.GetTypes().Where(type => + { + IEnumerable categories = type.GetCustomAttributes(); + return !categories.Any(); + }) + .Do(type => SafePatch(harmony, type)); } - catch (Exception ex) + + /// + /// Attempts to safely patch a , logging any errors. + /// + /// The to attempt to patch. + public void SafePatch(Type type) { - Logger.Error($"[HarmonyExtensions] failed to safely patch {harmony.Id} ({type.FullName}): {ex}"); + try + { + harmony.CreateClassProcessor(type).Patch(); + } + catch (Exception ex) + { + Logger.Error($"[HarmonyExtensions] failed to safely patch {harmony.Id} ({type.FullName}): {ex}"); + } } } } diff --git a/SecretAPI/Extensions/MirrorExtensions.cs b/SecretAPI/Extensions/MirrorExtensions.cs index 126321e..30aea4e 100644 --- a/SecretAPI/Extensions/MirrorExtensions.cs +++ b/SecretAPI/Extensions/MirrorExtensions.cs @@ -11,26 +11,6 @@ /// public static class MirrorExtensions { - /// - /// Sends a fake cassie message to a player. - /// - /// The target to send the cassie message to. - /// The message to send. - /// Whether the cassie is held. - /// Whether the cassie is noisy. - /// Whether there is subtitles on the cassie. - /// The custom subtitles to use for the cassie. - [Obsolete("Due to NW changes to Cassie, this is no longer functional.")] - public static void SendFakeCassieMessage( - this Player target, - string message, - bool isHeld = false, - bool isNoisy = true, - bool isSubtitles = true, - string customSubtitles = "") - { - } - /// /// Send a fake rpc message to a player. /// diff --git a/SecretAPI/Extensions/PlayerExtensions.cs b/SecretAPI/Extensions/PlayerExtensions.cs index 032b3bf..d2a6737 100644 --- a/SecretAPI/Extensions/PlayerExtensions.cs +++ b/SecretAPI/Extensions/PlayerExtensions.cs @@ -2,9 +2,6 @@ { using CustomPlayerEffects; using Interactables.Interobjects.DoorUtils; - using InventorySystem; - using InventorySystem.Items; - using InventorySystem.Items.Usables.Scp330; using LabApi.Features.Wrappers; using SecretAPI.Enums; @@ -13,74 +10,73 @@ /// public static class PlayerExtensions { - /// - /// Gets an effect of a player based on the effect name. - /// /// The player to get effect from. - /// Name of the effect to find. - /// The effect. - public static StatusEffectBase GetEffect(this Player player, string name) - => player.ReferenceHub.playerEffectsController.TryGetEffect(name, out StatusEffectBase? effect) ? effect : null!; - - /// - /// Checks whether a player has permission to access a . - /// - /// The player to check. - /// The requester to check for permissions. - /// The to use for checking if a player has it. - /// Whether a valid permission was found. - public static bool HasDoorPermission(this Player player, IDoorPermissionRequester requester, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) + extension(Player player) { - if (checkFlags.HasFlag(DoorPermissionCheck.Bypass) && player.IsBypassEnabled) - return true; - - if (checkFlags.HasFlag(DoorPermissionCheck.Role) && player.RoleBase is IDoorPermissionProvider roleProvider && requester.PermissionsPolicy.CheckPermissions(roleProvider.GetPermissions(requester))) - return true; + /// + /// Gets an effect of a player based on the effect name. + /// + /// Name of the effect to find. + /// The effect. + public StatusEffectBase GetEffect(string name) + => player.ReferenceHub.playerEffectsController.TryGetEffect(name, out StatusEffectBase? effect) ? effect : null!; - foreach (Item item in player.Items) + /// + /// Checks whether a player has permission to access a . + /// + /// The requester to check for permissions. + /// The to use for checking if a player has it. + /// Whether a valid permission was found. + public bool HasDoorPermission(IDoorPermissionRequester requester, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) { - bool isCurrent = item == player.CurrentItem; - if (!checkFlags.HasFlag(DoorPermissionCheck.CurrentItem) && isCurrent) - continue; - - if (!checkFlags.HasFlag(DoorPermissionCheck.InventoryExcludingCurrent) && !isCurrent) - continue; + if (checkFlags.HasFlag(DoorPermissionCheck.Bypass) && player.IsBypassEnabled) + return true; - if (item.Base is IDoorPermissionProvider itemProvider && requester.PermissionsPolicy.CheckPermissions(itemProvider.GetPermissions(requester))) + if (checkFlags.HasFlag(DoorPermissionCheck.Role) && player.RoleBase is IDoorPermissionProvider roleProvider && requester.PermissionsPolicy.CheckPermissions(roleProvider.GetPermissions(requester))) return true; - } - return false; - } + foreach (Item item in player.Items) + { + bool isCurrent = item == player.CurrentItem; + if (!checkFlags.HasFlag(DoorPermissionCheck.CurrentItem) && isCurrent) + continue; - /// - /// Checks whether a player has permission to access a . - /// - /// The player to check. - /// The door to check for permissions. - /// The to use for checking if a player has it. - /// Whether a valid permission was found. - public static bool HasDoorPermission(this Player player, Door door, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) - => player.HasDoorPermission(door.Base, checkFlags); + if (!checkFlags.HasFlag(DoorPermissionCheck.InventoryExcludingCurrent) && !isCurrent) + continue; - /// - /// Checks whether a player has permission to access a . - /// - /// The player to check. - /// The locker chamber to check for permissions. - /// The to use for checking if a player has it. - /// Whether a valid permission was found. - public static bool HasLockerChamberPermission(this Player player, LockerChamber chamber, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) - => player.HasDoorPermission(chamber.Base, checkFlags); + if (item.Base is IDoorPermissionProvider itemProvider && requester.PermissionsPolicy.CheckPermissions(itemProvider.GetPermissions(requester))) + return true; + } - /// - /// Checks whether a player has permission to access a . - /// - /// The player to check. - /// The generator to check for permissions. - /// The to use for checking if a player has it. - /// Whether a valid permission was found. - public static bool HasGeneratorPermission(this Player player, Generator generator, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) - => player.HasDoorPermission(generator.Base, checkFlags); + return false; + } + + /// + /// Checks whether a player has permission to access a . + /// + /// The door to check for permissions. + /// The to use for checking if a player has it. + /// Whether a valid permission was found. + public bool HasDoorPermission(Door door, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) + => player.HasDoorPermission(door.Base, checkFlags); + + /// + /// Checks whether a player has permission to access a . + /// + /// The locker chamber to check for permissions. + /// The to use for checking if a player has it. + /// Whether a valid permission was found. + public bool HasLockerChamberPermission(LockerChamber chamber, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) + => player.HasDoorPermission(chamber.Base, checkFlags); + + /// + /// Checks whether a player has permission to access a . + /// + /// The generator to check for permissions. + /// The to use for checking if a player has it. + /// Whether a valid permission was found. + public bool HasGeneratorPermission(Generator generator, DoorPermissionCheck checkFlags = DoorPermissionCheck.Default) + => player.HasDoorPermission(generator.Base, checkFlags); + } } } \ No newline at end of file diff --git a/SecretAPI/Extensions/RoleExtensions.cs b/SecretAPI/Extensions/RoleExtensions.cs deleted file mode 100644 index e436281..0000000 --- a/SecretAPI/Extensions/RoleExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace SecretAPI.Extensions -{ - using System; - using System.Diagnostics.CodeAnalysis; - using InventorySystem; - using LabApi.Features.Extensions; - using PlayerRoles; - using Respawning.Objectives; - using UnityEngine; - - /// - /// Extensions related to . - /// - [Obsolete("This no longer provides anything that basegame/LabAPI does not")] - public static class RoleExtensions - { - /// - /// Tries to get a role base from a . - /// - /// The to get base of. - /// The found. - /// The . - /// The role base found, else null. - [Obsolete("Use LabApi.Features.Extensions.RoleExtensions.TryGetRoleBase")] - public static bool TryGetRoleBase(this RoleTypeId roleTypeId, [NotNullWhen(true)] out T? role) - => LabApi.Features.Extensions.RoleExtensions.TryGetRoleBase(roleTypeId, out role); - - /// - /// Gets the color of a . - /// - /// The role to get color of. - /// The color found, if not found then white. - [Obsolete("Use Respawning.Objectives.GetRoleColor")] - public static Color GetColor(this RoleTypeId roleTypeId) - => roleTypeId.GetRoleColor(); - - /// - /// Tries to get a random spawn point from a . - /// - /// The role to get spawn from. - /// The position found. - /// The rotation found. - /// Whether a spawnpoint was found. - [Obsolete("Use LabApi.Features.Extensions.RoleExtensions.TryGetRandomSpawnPoint")] - public static bool GetRandomSpawnPosition(this RoleTypeId role, out Vector3 position, out float horizontalRot) - => role.TryGetRandomSpawnPoint(out position, out horizontalRot); - - /// - /// Gets the inventory of the specified . - /// - /// The . - /// The found. - [Obsolete("Use LabApi.Features.Extensions.RoleExtensions.GetInventory")] - public static InventoryRoleInfo GetInventory(this RoleTypeId role) - => LabApi.Features.Extensions.RoleExtensions.GetInventory(role); - } -} \ No newline at end of file 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..ab2a7a9 --- /dev/null +++ b/SecretAPI/Features/Commands/CustomCommand.cs @@ -0,0 +1,55 @@ +namespace SecretAPI.Features.Commands +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + 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!"); + } + + /// + /// Checks whether an argument matches any . + /// + /// The argument to check if is reference to subcommand. + /// The command found, otherwise false. + /// Whether a sub command matching the argument was found. + protected bool CheckSubCommand(string argument, [NotNullWhen(true)] out CustomCommand? command) + { + foreach (CustomCommand subCommand in SubCommands) + { + if (subCommand.Command == argument || subCommand.Aliases.Any(alias => alias == argument)) + { + command = subCommand; + return true; + } + } + + command = null; + return false; + } + } +} \ 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/Features/Effects/CustomPlayerEffect.cs b/SecretAPI/Features/Effects/CustomPlayerEffect.cs index 6231060..8f9b080 100644 --- a/SecretAPI/Features/Effects/CustomPlayerEffect.cs +++ b/SecretAPI/Features/Effects/CustomPlayerEffect.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using CustomPlayerEffects; using LabApi.Features.Wrappers; - using SecretAPI.Attribute; + using SecretAPI.Attributes; using SecretAPI.Extensions; using UnityEngine; using UnityEngine.SceneManagement; diff --git a/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs b/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs index dcc3c58..d7b94cb 100644 --- a/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs +++ b/SecretAPI/Patches/Features/SendSettingsPlayerSync.cs @@ -2,7 +2,7 @@ { using HarmonyLib; using LabApi.Features.Wrappers; - using SecretAPI.Attribute; + using SecretAPI.Attributes; using SecretAPI.Features.UserSettings; using UserSettings.ServerSpecific; diff --git a/SecretAPI/Patches/Features/SendSettingsServerSync.cs b/SecretAPI/Patches/Features/SendSettingsServerSync.cs index cb2fd52..e84d1ff 100644 --- a/SecretAPI/Patches/Features/SendSettingsServerSync.cs +++ b/SecretAPI/Patches/Features/SendSettingsServerSync.cs @@ -1,7 +1,7 @@ namespace SecretAPI.Patches.Features { using HarmonyLib; - using SecretAPI.Attribute; + using SecretAPI.Attributes; using SecretAPI.Features.UserSettings; using UserSettings.ServerSpecific; diff --git a/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs b/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs index f17b81c..a1b1e74 100644 --- a/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs +++ b/SecretAPI/Patches/Features/SettingsOriginalDefinitionFix.cs @@ -1,7 +1,7 @@ namespace SecretAPI.Patches.Features { using HarmonyLib; - using SecretAPI.Attribute; + using SecretAPI.Attributes; using SecretAPI.Features.UserSettings; using UserSettings.ServerSpecific; diff --git a/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs b/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs index 89e08a9..39cf781 100644 --- a/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs +++ b/SecretAPI/Patches/Features/SettingsSyncValidateFix.cs @@ -1,7 +1,7 @@ namespace SecretAPI.Patches.Features { using HarmonyLib; - using SecretAPI.Attribute; + using SecretAPI.Attributes; using SecretAPI.Features.UserSettings; using UserSettings.ServerSpecific; diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index ba3d4b3..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,17 +25,25 @@ True \ + + True + analyzers/dotnet/cs + + + + + - + diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 6272099..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.Attribute; /// /// 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(); } ///