From e0137647f01f4b093053d7adc08300af28009e3c Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 27 Apr 2026 15:59:56 +0200 Subject: [PATCH 01/18] SourceGeneration --- .../AnalyzerReleases.Shipped.md | 3 + .../AnalyzerReleases.Unshipped.md | 7 + .../Builders/Builder.cs | 19 +++ .../Builders/ClassBuilder.cs | 60 +++++++ .../Builders/MethodBuilder.cs | 44 +++++ SecretAPI.SourceGenerators/Diagnostics.cs | 28 ++++ .../Generators/CallOnLoadGenerator.cs | 152 ++++++++++++++++++ SecretAPI.SourceGenerators/GlobalUsings.cs | 19 +++ .../SecretAPI.SourceGenerators.csproj | 26 +++ .../Utils/GeneratedIdentifyUtils.cs | 19 +++ .../Utils/GenericTypeUtils.cs | 12 ++ .../Utils/MethodParameter.cs | 40 +++++ .../Utils/MethodUtils.cs | 20 +++ SecretAPI.SourceGenerators/Utils/TypeUtils.cs | 10 ++ SecretAPI.sln | 6 + SecretAPI/SecretAPI.csproj | 2 + SecretAPI/SecretApi.cs | 2 +- 17 files changed, 468 insertions(+), 1 deletion(-) create mode 100644 SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md create mode 100644 SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md create mode 100644 SecretAPI.SourceGenerators/Builders/Builder.cs create mode 100644 SecretAPI.SourceGenerators/Builders/ClassBuilder.cs create mode 100644 SecretAPI.SourceGenerators/Builders/MethodBuilder.cs create mode 100644 SecretAPI.SourceGenerators/Diagnostics.cs create mode 100644 SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs create mode 100644 SecretAPI.SourceGenerators/GlobalUsings.cs create mode 100644 SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj create mode 100644 SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs create mode 100644 SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs create mode 100644 SecretAPI.SourceGenerators/Utils/MethodParameter.cs create mode 100644 SecretAPI.SourceGenerators/Utils/MethodUtils.cs create mode 100644 SecretAPI.SourceGenerators/Utils/TypeUtils.cs diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..60b59dd --- /dev/null +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,3 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..ad9f9dc --- /dev/null +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,7 @@ +### New Rules + + Rule ID | Category | Severity | Notes +------------|----------|----------|--------------------- + SecretGen0 | Usage | Error | CA6000_AnalyzerName + SecretGen1 | Usage | Error | CA6000_AnalyzerName + SecretGen2 | Usage | Error | CA6000_AnalyzerName \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Builders/Builder.cs b/SecretAPI.SourceGenerators/Builders/Builder.cs new file mode 100644 index 0000000..4cc8b63 --- /dev/null +++ b/SecretAPI.SourceGenerators/Builders/Builder.cs @@ -0,0 +1,19 @@ +namespace SecretAPI.SourceGenerators.Builders; + +/// +/// Base of a builder. +/// +/// The this is handling. +internal abstract class Builder + where TBuilder : Builder +{ + protected readonly List _modifiers = new(); + + internal TBuilder AddModifiers(params SyntaxKind[] modifiers) + { + foreach (SyntaxKind token in modifiers) + _modifiers.Add(Token(token)); + + return (TBuilder)this; + } +} \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs new file mode 100644 index 0000000..671cf80 --- /dev/null +++ b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs @@ -0,0 +1,60 @@ +namespace SecretAPI.SourceGenerators.Builders; + +internal class ClassBuilder : Builder +{ + 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.SourceGenerators/Builders/MethodBuilder.cs b/SecretAPI.SourceGenerators/Builders/MethodBuilder.cs new file mode 100644 index 0000000..6538e4a --- /dev/null +++ b/SecretAPI.SourceGenerators/Builders/MethodBuilder.cs @@ -0,0 +1,44 @@ +namespace SecretAPI.SourceGenerators.Builders; + +internal class MethodBuilder : Builder +{ + 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.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs new file mode 100644 index 0000000..8b3e2b4 --- /dev/null +++ b/SecretAPI.SourceGenerators/Diagnostics.cs @@ -0,0 +1,28 @@ +namespace SecretAPI.SourceGenerators; + +internal static class Diagnostics +{ + internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new( + "SecretGen0", + "Plugin class must be partial", + "Plugin class '{0}' is missing partial modifier", + "Usage", + DiagnosticSeverity.Error, + true); + + internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new( + "SecretGen1", + "Method must be accessible", + "Method '{0}' has accessibility '{1}', which is not supported for generated calls", + "Usage", + DiagnosticSeverity.Error, + true); + + internal static readonly DiagnosticDescriptor MustBeStaticMethod = new( + "SecretGen2", + "Method must be static", + "Method '{0}' is not marked as static", + "Usage", + DiagnosticSeverity.Error, + true); +} \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs new file mode 100644 index 0000000..7147eb0 --- /dev/null +++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs @@ -0,0 +1,152 @@ +namespace SecretAPI.SourceGenerators.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 ValidateMethod(SourceProductionContext context, Tuple pluginInfo, IMethodSymbol method) + { + bool isValid = true; + + if (!method.IsStatic) + { + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.MustBeStaticMethod, + method.Locations.FirstOrDefault(), + method.Name)); + + isValid = false; + } + + if (method.DeclaredAccessibility is Accessibility.Private) + { + context.ReportDiagnostic( + Diagnostic.Create( + Diagnostics.MustBeAccessibleMethod, + method.Locations.FirstOrDefault(), + method.Name, + method.DeclaredAccessibility)); + + isValid = false; + } + + return isValid; + } + + 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( + Diagnostics.MustBePartialPluginClass, + pluginInfo.Item1.GetLocation(), + pluginInfo.Item1.Identifier.Text + ) + ); + } + + IMethodSymbol[] loadCalls = methods + .Where(m => m.isLoad && ValidateMethod(context, pluginInfo, m.method)) + .Select(m => m.method) + .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation)) + .ToArray(); + + IMethodSymbol[] unloadCalls = methods + .Where(m => m.isUnload && ValidateMethod(context, pluginInfo, m.method)) + .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.SourceGenerators/GlobalUsings.cs b/SecretAPI.SourceGenerators/GlobalUsings.cs new file mode 100644 index 0000000..eb69204 --- /dev/null +++ b/SecretAPI.SourceGenerators/GlobalUsings.cs @@ -0,0 +1,19 @@ +//? 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.SourceGenerators.Builders; +global using SecretAPI.SourceGenerators.Utils; + +//? Static utils from SecretAPI +global using static SecretAPI.SourceGenerators.Utils.GenericTypeUtils; +global using static SecretAPI.SourceGenerators.Utils.GeneratedIdentifyUtils; +global using static SecretAPI.SourceGenerators.Utils.MethodUtils; +global using static SecretAPI.SourceGenerators.Utils.TypeUtils; \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj new file mode 100644 index 0000000..b6bd4b5 --- /dev/null +++ b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj @@ -0,0 +1,26 @@ + + + + netstandard2.0 + 14 + true + enable + + + + true + false + Library + true + + + + + + + + + + + + diff --git a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs new file mode 100644 index 0000000..063d3b1 --- /dev/null +++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs @@ -0,0 +1,19 @@ +namespace SecretAPI.SourceGenerators.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.SourceGenerators/Utils/GenericTypeUtils.cs b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs new file mode 100644 index 0000000..f530632 --- /dev/null +++ b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs @@ -0,0 +1,12 @@ +namespace SecretAPI.SourceGenerators.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.SourceGenerators/Utils/MethodParameter.cs b/SecretAPI.SourceGenerators/Utils/MethodParameter.cs new file mode 100644 index 0000000..b2dd8db --- /dev/null +++ b/SecretAPI.SourceGenerators/Utils/MethodParameter.cs @@ -0,0 +1,40 @@ +namespace SecretAPI.SourceGenerators.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.SourceGenerators/Utils/MethodUtils.cs b/SecretAPI.SourceGenerators/Utils/MethodUtils.cs new file mode 100644 index 0000000..d0f4b05 --- /dev/null +++ b/SecretAPI.SourceGenerators/Utils/MethodUtils.cs @@ -0,0 +1,20 @@ +namespace SecretAPI.SourceGenerators.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.SourceGenerators/Utils/TypeUtils.cs b/SecretAPI.SourceGenerators/Utils/TypeUtils.cs new file mode 100644 index 0000000..1b59b10 --- /dev/null +++ b/SecretAPI.SourceGenerators/Utils/TypeUtils.cs @@ -0,0 +1,10 @@ +namespace SecretAPI.SourceGenerators.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.sln b/SecretAPI.sln index 4ed659a..ad97fde 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.SourceGenerators", "SecretAPI.SourceGenerators\SecretAPI.SourceGenerators.csproj", "{15C8D708-16DE-4533-AEB6-003C3EDCAD45}" +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 + {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Debug|Any CPU.Build.0 = Debug|Any CPU + {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Release|Any CPU.ActiveCfg = Release|Any CPU + {15C8D708-16DE-4533-AEB6-003C3EDCAD45}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index 0302f70..055b9de 100644 --- a/SecretAPI/SecretAPI.csproj +++ b/SecretAPI/SecretAPI.csproj @@ -31,6 +31,8 @@ + + diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 9033e9a..941f7b3 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"; From ff689db62795fd254cf33305e3409e957d4eec06 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:01:24 +0200 Subject: [PATCH 02/18] Removed unused class --- SecretAPI.SourceGenerators/GlobalUsings.cs | 1 - .../Utils/GeneratedIdentifyUtils.cs | 2 +- SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs | 12 ------------ 3 files changed, 1 insertion(+), 14 deletions(-) delete mode 100644 SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs diff --git a/SecretAPI.SourceGenerators/GlobalUsings.cs b/SecretAPI.SourceGenerators/GlobalUsings.cs index eb69204..8869bf3 100644 --- a/SecretAPI.SourceGenerators/GlobalUsings.cs +++ b/SecretAPI.SourceGenerators/GlobalUsings.cs @@ -13,7 +13,6 @@ global using SecretAPI.SourceGenerators.Utils; //? Static utils from SecretAPI -global using static SecretAPI.SourceGenerators.Utils.GenericTypeUtils; global using static SecretAPI.SourceGenerators.Utils.GeneratedIdentifyUtils; global using static SecretAPI.SourceGenerators.Utils.MethodUtils; global using static SecretAPI.SourceGenerators.Utils.TypeUtils; \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs index 063d3b1..da236f0 100644 --- a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs +++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs @@ -13,7 +13,7 @@ private static AttributeSyntax GetGeneratedCodeAttributeSyntax() 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.SourceGenerators/Utils/GenericTypeUtils.cs b/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs deleted file mode 100644 index f530632..0000000 --- a/SecretAPI.SourceGenerators/Utils/GenericTypeUtils.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace SecretAPI.SourceGenerators.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 From a9decf7a499b0b90b8c20fa932b5326e8a089e73 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:18:00 +0200 Subject: [PATCH 03/18] Remove unused pluginInfo parameter --- .../Generators/CallOnLoadGenerator.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs index 7147eb0..6171c97 100644 --- a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs +++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs @@ -69,7 +69,7 @@ private static int GetPriority(IMethodSymbol method, string attributeLocation) return 0; } - private static bool ValidateMethod(SourceProductionContext context, Tuple pluginInfo, IMethodSymbol method) + private static bool ValidateMethod(SourceProductionContext context, IMethodSymbol method) { bool isValid = true; @@ -119,13 +119,13 @@ private static void Generate( } IMethodSymbol[] loadCalls = methods - .Where(m => m.isLoad && ValidateMethod(context, pluginInfo, m.method)) + .Where(m => m.isLoad && ValidateMethod(context, m.method)) .Select(m => m.method) .OrderBy(m => GetPriority(m, CallOnLoadAttributeLocation)) .ToArray(); IMethodSymbol[] unloadCalls = methods - .Where(m => m.isUnload && ValidateMethod(context, pluginInfo, m.method)) + .Where(m => m.isUnload && ValidateMethod(context, m.method)) .Select(m => m.method) .OrderBy(m => GetPriority(m, CallOnUnloadAttributeLocation)) .ToArray(); From f6322bcc01477ec88c8a32d50b05abb4a5d00753 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:26:55 +0200 Subject: [PATCH 04/18] Rules --- SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md | 6 +++--- SecretAPI.SourceGenerators/Diagnostics.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md index ad9f9dc..08f562d 100644 --- a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -2,6 +2,6 @@ Rule ID | Category | Severity | Notes ------------|----------|----------|--------------------- - SecretGen0 | Usage | Error | CA6000_AnalyzerName - SecretGen1 | Usage | Error | CA6000_AnalyzerName - SecretGen2 | Usage | Error | CA6000_AnalyzerName \ No newline at end of file + SG001 | Usage | Error | MustBePartialPluginClass + SG002 | Usage | Error | MustBeAccessibleMethod + SG003 | Usage | Error | MustBeStaticMethod \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs index 8b3e2b4..21d8073 100644 --- a/SecretAPI.SourceGenerators/Diagnostics.cs +++ b/SecretAPI.SourceGenerators/Diagnostics.cs @@ -3,7 +3,7 @@ internal static class Diagnostics { internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new( - "SecretGen0", + "SG001", "Plugin class must be partial", "Plugin class '{0}' is missing partial modifier", "Usage", @@ -11,7 +11,7 @@ internal static class Diagnostics true); internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new( - "SecretGen1", + "SG002", "Method must be accessible", "Method '{0}' has accessibility '{1}', which is not supported for generated calls", "Usage", @@ -19,7 +19,7 @@ internal static class Diagnostics true); internal static readonly DiagnosticDescriptor MustBeStaticMethod = new( - "SecretGen2", + "SG003", "Method must be static", "Method '{0}' is not marked as static", "Usage", From de7fb2f34a504161ea0024ac3cd061dccac2de00 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 27 Apr 2026 16:37:47 +0200 Subject: [PATCH 05/18] Fix version number & OnLoad() not being utilized --- SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs | 6 ++++-- SecretAPI/SecretApi.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs index da236f0..02a4988 100644 --- a/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs +++ b/SecretAPI.SourceGenerators/Utils/GeneratedIdentifyUtils.cs @@ -2,6 +2,8 @@ internal static class GeneratedIdentifyUtils { + private static SyntaxToken CurrentVersion => Literal(typeof(GeneratedIdentifyUtils).Assembly.GetName().Version.ToString()); + private static AttributeSyntax GetGeneratedCodeAttributeSyntax() => Attribute(IdentifierName("GeneratedCode")) .WithArgumentList( @@ -9,9 +11,9 @@ private static AttributeSyntax GetGeneratedCodeAttributeSyntax() SeparatedList( new SyntaxNodeOrToken[] { - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.CodeGeneration"))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("SecretAPI.SourceGenerators"))), Token(SyntaxKind.CommaToken), - AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal("1.0.0"))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, CurrentVersion)), }))); internal static AttributeListSyntax GetGeneratedCodeAttributeListSyntax() diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 941f7b3..7c5931d 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -48,7 +48,7 @@ public partial class SecretApi : Plugin /// public override void Enable() { - CallOnLoadAttribute.Load(Assembly); + OnLoad(); } /// From 34cf80c9832fea052be2fbc41ba7f922c33d19e7 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:54:33 +0200 Subject: [PATCH 06/18] Fix: Add SourceGenerators as a project to solution --- SecretAPI.slnx | 1 + 1 file changed, 1 insertion(+) diff --git a/SecretAPI.slnx b/SecretAPI.slnx index 99c20fd..6995b49 100644 --- a/SecretAPI.slnx +++ b/SecretAPI.slnx @@ -1,4 +1,5 @@ + From ec9c08f96ec3eb3fd98680c55b91bd964695ad21 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Sat, 2 May 2026 22:29:17 +0200 Subject: [PATCH 07/18] Source Generate: Use SecretApiGenerated naming --- .../Builders/ClassBuilder.cs | 32 +++++++++++--- .../Generators/CallOnLoadGenerator.cs | 42 +++++-------------- SecretAPI/SecretApi.cs | 2 +- 3 files changed, 37 insertions(+), 39 deletions(-) diff --git a/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs index 671cf80..7f14306 100644 --- a/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs +++ b/SecretAPI.SourceGenerators/Builders/ClassBuilder.cs @@ -2,13 +2,13 @@ internal class ClassBuilder : Builder { - private NamespaceDeclarationSyntax _namespaceDeclaration; + private NamespaceDeclarationSyntax? _namespaceDeclaration; private ClassDeclarationSyntax _classDeclaration; private readonly List _usings = new(); private readonly List _methods = new(); - private ClassBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDeclarationSyntax classDeclaration) + private ClassBuilder(NamespaceDeclarationSyntax? namespaceDeclaration, ClassDeclarationSyntax classDeclaration) { _namespaceDeclaration = namespaceDeclaration; _classDeclaration = classDeclaration; @@ -16,12 +16,19 @@ private ClassBuilder(NamespaceDeclarationSyntax namespaceDeclaration, ClassDecla AddUsingStatements("System.CodeDom.Compiler"); } + private ClassBuilder(ClassDeclarationSyntax classDeclaration) + : this(null, classDeclaration) + { + } + 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 static ClassBuilder CreateBuilder(ClassDeclarationSyntax classDeclaration) => new(classDeclaration); + internal ClassBuilder AddUsingStatements(params string[] usingStatements) { foreach (string statement in usingStatements) @@ -46,14 +53,27 @@ internal CompilationUnitSyntax Build() .AddModifiers(_modifiers.ToArray()) .AddMembers(_methods.Cast().ToArray()); - _namespaceDeclaration = _namespaceDeclaration + _namespaceDeclaration = _namespaceDeclaration? .AddUsings(_usings.ToArray()) .AddMembers(_classDeclaration); - return CompilationUnit() - .AddMembers(_namespaceDeclaration) + CompilationUnitSyntax unit = CompilationUnit(); + + if (_namespaceDeclaration != null) + { + _namespaceDeclaration = _namespaceDeclaration + .AddUsings(_usings.ToArray()) + .AddMembers(_classDeclaration); + unit = unit.AddMembers(_namespaceDeclaration); + } + else + { + unit = unit.AddUsings(_usings.ToArray()).AddMembers(_classDeclaration); + } + + return unit .NormalizeWhitespace() - .WithLeadingTrivia(Comment("// "), LineFeed, Comment("#pragma warning disable"), LineFeed, Comment("#nullable enable"), LineFeed, LineFeed); + .WithLeadingTrivia(Comment("// "), LineFeed, LineFeed, Comment("#pragma warning disable"), LineFeed, Comment("#nullable enable"), LineFeed, LineFeed); } internal void Build(SourceProductionContext context, string name) => context.AddSource(name, Build().ToFullString()); diff --git a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs index 6171c97..dce991e 100644 --- a/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs +++ b/SecretAPI.SourceGenerators/Generators/CallOnLoadGenerator.cs @@ -2,12 +2,12 @@ /// /// Code generator for CallOnLoad/CallOnUnload +/// TODO: Implement IRegister source generation /// [Generator] public class CallOnLoadGenerator : IIncrementalGenerator { - private const string PluginNamespace = "LabApi.Loader.Features.Plugins"; - private const string PluginBaseClassName = "Plugin"; + private const string GeneratedClassName = "SecretApiGenerated"; private const string CallOnLoadAttributeLocation = "SecretAPI.Attributes.CallOnLoadAttribute"; private const string CallOnUnloadAttributeLocation = "SecretAPI.Attributes.CallOnUnloadAttribute"; @@ -27,19 +27,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) 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); - }); + context.RegisterSourceOutput(callProvider.Collect(), Generate); } private static bool HasAttribute(IMethodSymbol? method, string attributeLocation) @@ -101,23 +90,11 @@ private static bool ValidateMethod(SourceProductionContext context, IMethodSymbo 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) + if (methods.IsEmpty) return; - if (!pluginInfo.Item1.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) - { - context.ReportDiagnostic( - Diagnostic.Create( - Diagnostics.MustBePartialPluginClass, - pluginInfo.Item1.GetLocation(), - pluginInfo.Item1.Identifier.Text - ) - ); - } - IMethodSymbol[] loadCalls = methods .Where(m => m.isLoad && ValidateMethod(context, m.method)) .Select(m => m.method) @@ -133,20 +110,21 @@ private static void Generate( if (!loadCalls.Any() && !unloadCalls.Any()) return; - ClassBuilder classBuilder = ClassBuilder.CreateBuilder(pluginInfo.Item2) + // ClassBuilder classBuilder = ClassBuilder.CreateBuilder(pluginInfo.Item2) + ClassBuilder classBuilder = ClassBuilder.CreateBuilder(ClassDeclaration(GeneratedClassName)) .AddUsingStatements("System") - .AddModifiers(SyntaxKind.PartialKeyword); + .AddModifiers(SyntaxKind.InternalKeyword, SyntaxKind.StaticKeyword); classBuilder.StartMethodCreation("OnLoad", SyntaxKind.VoidKeyword) - .AddModifiers(SyntaxKind.PublicKeyword) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword) .AddStatements(MethodCallStatements(loadCalls)) .FinishMethodBuild(); classBuilder.StartMethodCreation("OnUnload", SyntaxKind.VoidKeyword) - .AddModifiers(SyntaxKind.PublicKeyword) + .AddModifiers(SyntaxKind.PublicKeyword, SyntaxKind.StaticKeyword) .AddStatements(MethodCallStatements(unloadCalls)) .FinishMethodBuild(); - classBuilder.Build(context, $"{pluginInfo.Item2.Name}.g.cs"); + classBuilder.Build(context, $"{GeneratedClassName}.g.cs"); } } \ No newline at end of file diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 7c5931d..1c44952 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -48,7 +48,7 @@ public partial class SecretApi : Plugin /// public override void Enable() { - OnLoad(); + SecretApiGenerated.OnLoad(); } /// From 9be338329c08cb500d81f3c2dcad9c26d3d3c747 Mon Sep 17 00:00:00 2001 From: Evelyn <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 14:53:28 +0200 Subject: [PATCH 08/18] Remove partial modifier --- SecretAPI/SecretApi.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SecretAPI/SecretApi.cs b/SecretAPI/SecretApi.cs index 1c44952..2a288bd 100644 --- a/SecretAPI/SecretApi.cs +++ b/SecretAPI/SecretApi.cs @@ -11,7 +11,7 @@ /// /// Main class handling loading API. /// -public partial class SecretApi : Plugin +public class SecretApi : Plugin { /// public override string Name => "SecretAPI"; @@ -56,4 +56,4 @@ public override void Disable() { Harmony.UnpatchAll(Harmony.Id); } -} \ No newline at end of file +} From c08364187294afd30e2546135fa30ed6dbb805ee Mon Sep 17 00:00:00 2001 From: Lumi Date: Mon, 4 May 2026 17:02:26 +0100 Subject: [PATCH 09/18] Merge pull request #98 * feat: admin toy extensions * feat: reorganisation * docs: fix T cref error * feat: item extensions * feat: pickup extensions * Remove .As, replaced with Cast and CastTypeSafely --- .../AdminToys/AdminToyExtensions.Camera.cs | 68 +++++++++++ .../AdminToyExtensions.Interactable.cs | 91 ++++++++++++++ .../AdminToys/AdminToyExtensions.Light.cs | 114 ++++++++++++++++++ .../AdminToys/AdminToyExtensions.Primitive.cs | 87 +++++++++++++ .../AdminToys/AdminToyExtensions.Text.cs | 35 ++++++ .../AdminToys/AdminToyExtensions.Waypoint.cs | 46 +++++++ .../AdminToys/AdminToyExtensions.cs | 60 +++++++++ .../Items/ItemExtensions.Firearms.cs | 57 +++++++++ .../Items/ItemExtensions.MicroHID.cs | 57 +++++++++ .../Extensions/Items/ItemExtensions.Scp127.cs | 35 ++++++ .../Items/ItemExtensions.Scp1509.cs | 67 ++++++++++ SecretAPI/Extensions/Items/ItemExtensions.cs | 21 ++++ .../Pickups/PickupExtensions.Grenades.cs | 86 +++++++++++++ .../Pickups/PickupExtensions.Jailbird.cs | 46 +++++++ .../Pickups/PickupExtensions.MicroHID.cs | 46 +++++++ .../Pickups/PickupExtensions.Radio.cs | 46 +++++++ .../Extensions/Pickups/PickupExtensions.cs | 61 ++++++++++ SecretAPI/Extensions/ReflectionExtensions.cs | 22 ++++ 18 files changed, 1045 insertions(+) create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Camera.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Interactable.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Light.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Primitive.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Text.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.Waypoint.cs create mode 100644 SecretAPI/Extensions/AdminToys/AdminToyExtensions.cs create mode 100644 SecretAPI/Extensions/Items/ItemExtensions.Firearms.cs create mode 100644 SecretAPI/Extensions/Items/ItemExtensions.MicroHID.cs create mode 100644 SecretAPI/Extensions/Items/ItemExtensions.Scp127.cs create mode 100644 SecretAPI/Extensions/Items/ItemExtensions.Scp1509.cs create mode 100644 SecretAPI/Extensions/Items/ItemExtensions.cs create mode 100644 SecretAPI/Extensions/Pickups/PickupExtensions.Grenades.cs create mode 100644 SecretAPI/Extensions/Pickups/PickupExtensions.Jailbird.cs create mode 100644 SecretAPI/Extensions/Pickups/PickupExtensions.MicroHID.cs create mode 100644 SecretAPI/Extensions/Pickups/PickupExtensions.Radio.cs create mode 100644 SecretAPI/Extensions/Pickups/PickupExtensions.cs diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Camera.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Camera.cs new file mode 100644 index 0000000..dbb5996 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Camera.cs @@ -0,0 +1,68 @@ +namespace SecretAPI.Extensions.AdminToys; + +using LabApi.Features.Wrappers; +using UnityEngine; + +/// +/// Extensions for camera toys. +/// +public static partial class AdminToyExtensions +{ + extension(CameraToy toy) + { + /// + /// Modify the label of this . + /// + /// The label. + /// The modified . + public CameraToy WithLabel(string label) + { + toy.Label = label; + return toy; + } + + /// + /// Modify the room of this . + /// + /// The room. + /// The modified . + public CameraToy WithRoom(Room room) + { + toy.Room = room; + return toy; + } + + /// + /// Modify the vertical constrains of this . + /// + /// The constraints. + /// The modified . + public CameraToy WithVerticalConstraints(Vector2 constraint) + { + toy.VerticalConstraints = constraint; + return toy; + } + + /// + /// Modify the horizontal constrains of this . + /// + /// The constraints. + /// The modified . + public CameraToy WithHorizontalConstraints(Vector2 constraint) + { + toy.HorizontalConstraint = constraint; + return toy; + } + + /// + /// Modify the zoom constrains of this . + /// + /// The constraints. + /// The modified . + public CameraToy WithZoomConstraints(Vector2 constraint) + { + toy.ZoomConstraints = constraint; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Interactable.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Interactable.cs new file mode 100644 index 0000000..e5394fe --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Interactable.cs @@ -0,0 +1,91 @@ +namespace SecretAPI.Extensions.AdminToys; + +using System; +using global::AdminToys; +using LabApi.Features.Wrappers; + +/// +/// Extensions for interactable toys. +/// +public static partial class AdminToyExtensions +{ + extension(InteractableToy toy) + { + /// + /// Call an when this is interacted with. + /// + /// The to run. + /// The modified . + public InteractableToy WhenInteracted(Action action) + { + toy.OnInteracted += action; + return toy; + } + + /// + /// Call an when this is being searched. + /// + /// The to run. + /// The modified . + public InteractableToy WhenSearching(Action action) + { + toy.OnSearching += action; + return toy; + } + + /// + /// Call an when this has been searched. + /// + /// The to run. + /// The modified . + public InteractableToy WhenSearched(Action action) + { + toy.OnSearched += action; + return toy; + } + + /// + /// Call an when this has had an aborted search. + /// + /// The to run. + /// The modified . + public InteractableToy WhenSearchAborted(Action action) + { + toy.OnSearchAborted += action; + return toy; + } + + /// + /// Modify the shape of this . + /// + /// The shape. + /// The modified . + public InteractableToy WithShape(InvisibleInteractableToy.ColliderShape shape) + { + toy.Shape = shape; + return toy; + } + + /// + /// Modify how long a user should interact with before triggering the event. + /// + /// The duration. + /// The modified . + public InteractableToy WithInteractionDuration(float duration) + { + toy.InteractionDuration = duration; + return toy; + } + + /// + /// Modify whether this can be interacted with. + /// + /// The lock state. + /// The modified . + public InteractableToy IsLocked(bool locked) + { + toy.IsLocked = locked; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Light.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Light.cs new file mode 100644 index 0000000..fd4ff69 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Light.cs @@ -0,0 +1,114 @@ +namespace SecretAPI.Extensions.AdminToys; + +using LabApi.Features.Wrappers; +using UnityEngine; + +/// +/// Extensions for Light Source Toys. +/// +public static partial class AdminToyExtensions +{ + extension(LightSourceToy toy) + { + /// + /// Modify the intensity of this . + /// + /// The intensity. + /// The modified . + public LightSourceToy WithIntensity(float intensity) + { + toy.Intensity = intensity; + return toy; + } + + /// + /// Modify the range of this . + /// + /// The range. + /// The modified . + public LightSourceToy WithRange(float range) + { + toy.Range = range; + return toy; + } + + /// + /// Modify the color of this . + /// + /// The color. + /// The modified . + public LightSourceToy WithColor(Color color) + { + toy.Color = color; + return toy; + } + + /// + /// Modify the shadow type of this . + /// + /// The shadow type. + /// The modified . + public LightSourceToy WithShadowType(LightShadows type) + { + toy.ShadowType = type; + return toy; + } + + /// + /// Modify the shadow strength of this . + /// + /// The strength. + /// The modified . + public LightSourceToy WithShadowStrength(float strength) + { + toy.ShadowStrength = strength; + return toy; + } + + /// + /// Modify the type of this . + /// + /// The type. + /// The modified . + public LightSourceToy WithType(LightType lightType) + { + toy.Type = lightType; + return toy; + } + + /// + /// Modify the shape of this . + /// + /// The shape. + /// The modified . +#pragma warning disable CS0618 // Type or member is obsolete + public LightSourceToy WithShape(LightShape shape) +#pragma warning restore CS0618 // Type or member is obsolete + { + toy.Shape = shape; + return toy; + } + + /// + /// Modify the spot angle of this . + /// + /// The angle. + /// The modified . + public LightSourceToy WithSpotAngle(float angle) + { + toy.SpotAngle = angle; + return toy; + } + + /// + /// Modify the inner spot angle of this . + /// + /// The angle. + /// The modified . + public LightSourceToy WithInnerSpotAngle(float angle) + { + toy.InnerSpotAngle = angle; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Primitive.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Primitive.cs new file mode 100644 index 0000000..f7ff3c5 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Primitive.cs @@ -0,0 +1,87 @@ +namespace SecretAPI.Extensions.AdminToys; + +using global::AdminToys; +using UnityEngine; +using PrimitiveObjectToy = LabApi.Features.Wrappers.PrimitiveObjectToy; + +/// +/// Extensions for primitive object toys. +/// +public static partial class AdminToyExtensions +{ + extension(PrimitiveObjectToy toy) + { + /// + /// Gets or sets a value indicating whether this is collidable. + /// + public bool Collidable + { + get => (toy.Flags & PrimitiveFlags.Collidable) == PrimitiveFlags.Collidable; + set => toy.Flags = value ? toy.Flags | PrimitiveFlags.Collidable : toy.Flags & ~PrimitiveFlags.Collidable; + } + + /// + /// Gets or sets a value indicating whether this is visible. + /// + public bool Visible + { + get => (toy.Flags & PrimitiveFlags.Visible) == PrimitiveFlags.Visible; + set => toy.Flags = value ? toy.Flags | PrimitiveFlags.Visible : toy.Flags & ~PrimitiveFlags.Visible; + } + + /// + /// Modify whether this is collidable. + /// + /// The collidable state. + /// The modified . + public PrimitiveObjectToy WithCollidable(bool collidable) + { + toy.Collidable = collidable; + return toy; + } + + /// + /// Modify whether this is visible. + /// + /// The visibility state. + /// The modified . + public PrimitiveObjectToy WithVisible(bool visible) + { + toy.Visible = visible; + return toy; + } + + /// + /// Modify the type of this . + /// + /// The type. + /// The modified . + public PrimitiveObjectToy WithType(PrimitiveType type) + { + toy.Type = type; + return toy; + } + + /// + /// Modify the color of this . + /// + /// The color. + /// The modified . + public PrimitiveObjectToy WithColor(Color color) + { + toy.Color = color; + return toy; + } + + /// + /// Modify the flags of this . + /// + /// The flags. + /// The modified . + public PrimitiveObjectToy WithFlags(PrimitiveFlags flags) + { + toy.Flags = flags; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Text.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Text.cs new file mode 100644 index 0000000..0a3a743 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Text.cs @@ -0,0 +1,35 @@ +namespace SecretAPI.Extensions.AdminToys; + +using LabApi.Features.Wrappers; +using UnityEngine; + +/// +/// Extension methods for text toys. +/// +public static partial class AdminToyExtensions +{ + extension(TextToy toy) + { + /// + /// Modify the text of this . + /// + /// The text. + /// The modified . + public TextToy WithText(string text) + { + toy.TextFormat = text; + return toy; + } + + /// + /// Modify the size of this . + /// + /// The size. + /// The modified . + public TextToy WithSize(Vector2 size) + { + toy.DisplaySize = size; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Waypoint.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Waypoint.cs new file mode 100644 index 0000000..0e4e906 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.Waypoint.cs @@ -0,0 +1,46 @@ +namespace SecretAPI.Extensions.AdminToys; + +using LabApi.Features.Wrappers; +using UnityEngine; + +/// +/// Extension methods for waypoint toys. +/// +public static partial class AdminToyExtensions +{ + extension(WaypointToy toy) + { + /// + /// Modify the bounds size of this . + /// + /// The size. + /// The modified . + public WaypointToy WithBoundsSize(Vector3 size) + { + toy.BoundsSize = size; + return toy; + } + + /// + /// Modify whether this has visible bounds. + /// + /// The visibility state. + /// The modified . + public WaypointToy WithVisualiseBounds(bool visible) + { + toy.VisualizeBounds = visible; + return toy; + } + + /// + /// Modify the priority bias of this . + /// + /// The bias. + /// The modified . + public WaypointToy WithPriorityBias(float bias) + { + toy.PriorityBias = bias; + return toy; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/AdminToys/AdminToyExtensions.cs b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.cs new file mode 100644 index 0000000..68e3a73 --- /dev/null +++ b/SecretAPI/Extensions/AdminToys/AdminToyExtensions.cs @@ -0,0 +1,60 @@ +namespace SecretAPI.Extensions.AdminToys; + +using LabApi.Features.Wrappers; +using UnityEngine; +using CapybaraToy = LabApi.Features.Wrappers.CapybaraToy; + +/// +/// Extensions for admin toys. +/// +public static partial class AdminToyExtensions +{ + extension(T toy) + where T : AdminToy + { + /// + /// Modify the position of this . + /// + /// The position to change to. + /// The modified . + public T WithPosition(Vector3 pos) + { + toy.Position = pos; + return toy; + } + + /// + /// Modify the rotation of this . + /// + /// The rotation to change to. + /// The modified . + public T WithRotation(Quaternion rot) + { + toy.Rotation = rot; + return toy; + } + + /// + /// Modify the scale of this . + /// + /// The scale to change to. + /// The modified . + public T WithScale(Vector3 scale) + { + toy.Scale = scale; + return toy; + } + } + + /// + /// Updates whether the capybara has enabled colliders. + /// + /// The . + /// Whether the colliders are enabled. + /// The modified . + public static CapybaraToy WithCollidersEnabled(this CapybaraToy toy, bool enabled) + { + toy.CollidersEnabled = enabled; + return toy; + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Items/ItemExtensions.Firearms.cs b/SecretAPI/Extensions/Items/ItemExtensions.Firearms.cs new file mode 100644 index 0000000..fbe50e6 --- /dev/null +++ b/SecretAPI/Extensions/Items/ItemExtensions.Firearms.cs @@ -0,0 +1,57 @@ +namespace SecretAPI.Extensions.Items; + +using LabApi.Features.Wrappers; + +/// +/// Extensions for firearms. +/// +public static partial class ItemExtensions +{ + extension(T item) + where T : FirearmItem + { + /// + /// Modifies the stored ammo in this . + /// + /// The ammo. + /// The modified . + public T WithStoredAmmo(int ammo) + { + item.StoredAmmo = ammo; + return item; + } + + /// + /// Modifies whether the magazine is inserted in this . + /// + /// Whether the magazine is inserted. + /// The modified . + public T WithMagazineInserted(bool magazineInserted) + { + item.MagazineInserted = magazineInserted; + return item; + } + + /// + /// Modifies whether the firearm is cocked in this . + /// + /// Whether the firearm is cocked. + /// The modified . + public T WithCocked(bool cocked) + { + item.Cocked = cocked; + return item; + } + + /// + /// Modifies the chambered ammo in this . + /// + /// The ammo. + /// The modified . + public T WithChamberedAmmo(int ammo) + { + item.ChamberedAmmo = ammo; + return item; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Items/ItemExtensions.MicroHID.cs b/SecretAPI/Extensions/Items/ItemExtensions.MicroHID.cs new file mode 100644 index 0000000..c2977a6 --- /dev/null +++ b/SecretAPI/Extensions/Items/ItemExtensions.MicroHID.cs @@ -0,0 +1,57 @@ +namespace SecretAPI.Extensions.Items; + +using InventorySystem.Items.MicroHID.Modules; +using LabApi.Features.Wrappers; + +/// +/// Extensions for Micro H.I.D. +/// +public static partial class ItemExtensions +{ + extension(MicroHIDItem item) + { + /// + /// Modify the energy of this . + /// + /// The energy. + /// The modified . + public MicroHIDItem WithEnergy(float energy) + { + item.Energy = energy; + return item; + } + + /// + /// Modify the broken state of this . + /// + /// Whether the item is broken or not. + /// The modified . + public MicroHIDItem WithBrokenState(bool brokenState) + { + item.IsBroken = brokenState; + return item; + } + + /// + /// Modify the phase of this . + /// + /// The phase. + /// The modified . + public MicroHIDItem WithPhase(MicroHidPhase phase) + { + item.Phase = phase; + return item; + } + + /// + /// Modify the firing mode of this . + /// + /// The firing mode. + /// The modified . + public MicroHIDItem WithFiringMode(MicroHidFiringMode mode) + { + item.FiringMode = mode; + return item; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Items/ItemExtensions.Scp127.cs b/SecretAPI/Extensions/Items/ItemExtensions.Scp127.cs new file mode 100644 index 0000000..1f1ba8c --- /dev/null +++ b/SecretAPI/Extensions/Items/ItemExtensions.Scp127.cs @@ -0,0 +1,35 @@ +namespace SecretAPI.Extensions.Items; + +using InventorySystem.Items.Firearms.Modules.Scp127; +using LabApi.Features.Wrappers; + +/// +/// Extensions for SCP-127. +/// +public static partial class ItemExtensions +{ + extension(Scp127Firearm item) + { + /// + /// Modifies the tier of this . + /// + /// The tier. + /// The modified . + public Scp127Firearm WithTier(Scp127Tier tier) + { + item.Tier = tier; + return item; + } + + /// + /// Modifies the experience of this . + /// + /// The experience. + /// The modified . + public Scp127Firearm WithExperience(float experience) + { + item.Experience = experience; + return item; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Items/ItemExtensions.Scp1509.cs b/SecretAPI/Extensions/Items/ItemExtensions.Scp1509.cs new file mode 100644 index 0000000..d78780a --- /dev/null +++ b/SecretAPI/Extensions/Items/ItemExtensions.Scp1509.cs @@ -0,0 +1,67 @@ +namespace SecretAPI.Extensions.Items; + +using LabApi.Features.Wrappers; + +/// +/// Extensions for SCP-1509. +/// +public static partial class ItemExtensions +{ + extension(Scp1509Item item) + { + /// + /// Modifies the shield regen rate of this . + /// + /// The rate. + /// The modified . + public Scp1509Item WithShieldRegenRate(float rate) + { + item.ShieldRegenRate = rate; + return item; + } + + /// + /// Modifies the shield decay rate of this . + /// + /// The rate. + /// The modified . + public Scp1509Item WithShieldDecayRate(float rate) + { + item.ShieldDecayRate = rate; + return item; + } + + /// + /// Modifies the unequip decay delay of this . + /// + /// The time to start decaying. + /// The modified . + public Scp1509Item WithUnequipDecayDelay(float time) + { + item.UnequipDecayDelay = time; + return item; + } + + /// + /// Modifies the revive cooldown of this . + /// + /// The cooldown. + /// The modified . + public Scp1509Item WithReviveCooldown(double cooldown) + { + item.ReviveCooldown = cooldown; + return item; + } + + /// + /// Modifies the equipped HS of this . + /// + /// The hume shield. + /// The modified . + public Scp1509Item WithEquippedHumeShield(float hs) + { + item.EquippedHS = hs; + return item; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Items/ItemExtensions.cs b/SecretAPI/Extensions/Items/ItemExtensions.cs new file mode 100644 index 0000000..ac671c9 --- /dev/null +++ b/SecretAPI/Extensions/Items/ItemExtensions.cs @@ -0,0 +1,21 @@ +namespace SecretAPI.Extensions.Items; + +using LabApi.Features.Wrappers; + +/// +/// Extensions for items. +/// +public static partial class ItemExtensions +{ + /// + /// Modifies the battery percentage of this . + /// + /// The . + /// The percentage. + /// The modified . + public static RadioItem WithBatteryPercent(this RadioItem item, byte battery) + { + item.BatteryPercent = battery; + return item; + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Pickups/PickupExtensions.Grenades.cs b/SecretAPI/Extensions/Pickups/PickupExtensions.Grenades.cs new file mode 100644 index 0000000..152b6fa --- /dev/null +++ b/SecretAPI/Extensions/Pickups/PickupExtensions.Grenades.cs @@ -0,0 +1,86 @@ +namespace SecretAPI.Extensions.Pickups; + +using LabApi.Features.Wrappers; +using UnityEngine; + +/// +/// Extensions for grenade projectiles. +/// +public static partial class PickupExtensions +{ + extension(T pickup) + where T : TimedGrenadeProjectile + { + /// + /// Modifies the remaining time of this . + /// + /// The time. + /// The modified . + public T WithRemainingTime(double time) + { + pickup.RemainingTime = time; + return pickup; + } + } + + extension(ExplosiveGrenadeProjectile pickup) + { + /// + /// Modifies the max explosion radius of this . + /// + /// The radius. + /// The modified . + public ExplosiveGrenadeProjectile WithMaxRadius(float radius) + { + pickup.MaxRadius = radius; + return pickup; + } + + /// + /// Modifies the scp damage multiplier of this . + /// + /// The multiplier. + /// The modified . + public ExplosiveGrenadeProjectile WithScpDamageMultiplier(float multiplier) + { + pickup.ScpDamageMultiplier = multiplier; + return pickup; + } + } + + /// + /// Modifies the flash time of this . + /// + /// The flashbang. + /// The time. + /// The modified . + public static FlashbangProjectile WithBlindTime(this FlashbangProjectile pickup, float time) + { + pickup.BaseBlindTime = time; + return pickup; + } + + /// + /// Modifies the velocity of this . + /// + /// The SCP-018 instance. + /// The velocity. + /// The modified . + public static Scp018Projectile WithVelocity(this Scp018Projectile pickup, Vector3 velocity) + { + pickup.Velocity = velocity; + return pickup; + } + + /// + /// Modifies the lockdown duration of this . + /// + /// The SCP-2176 instance. + /// The duration. + /// The modified . + public static Scp2176Projectile WithLockdownDuration(this Scp2176Projectile pickup, float duration) + { + pickup.LockdownDuration = duration; + return pickup; + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Pickups/PickupExtensions.Jailbird.cs b/SecretAPI/Extensions/Pickups/PickupExtensions.Jailbird.cs new file mode 100644 index 0000000..8f1aad2 --- /dev/null +++ b/SecretAPI/Extensions/Pickups/PickupExtensions.Jailbird.cs @@ -0,0 +1,46 @@ +namespace SecretAPI.Extensions.Pickups; + +using InventorySystem.Items.Jailbird; +using JailbirdPickup = LabApi.Features.Wrappers.JailbirdPickup; + +/// +/// Extensions for jailbirds. +/// +public static partial class PickupExtensions +{ + extension(JailbirdPickup pickup) + { + /// + /// Modifies the total damage dealt of this . + /// + /// The damage. + /// The modified . + public JailbirdPickup WithTotalDamageDealt(float damage) + { + pickup.TotalDamageDealt += damage; + return pickup; + } + + /// + /// Modifies the total charges performed of this . + /// + /// The charges. + /// The modified . + public JailbirdPickup WithTotalChargesPerformed(int charges) + { + pickup.TotalChargesPerformed += charges; + return pickup; + } + + /// + /// Modifies the wear state of this . + /// + /// The state. + /// The modified . + public JailbirdPickup WithWearState(JailbirdWearState state) + { + pickup.WearState = state; + return pickup; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Pickups/PickupExtensions.MicroHID.cs b/SecretAPI/Extensions/Pickups/PickupExtensions.MicroHID.cs new file mode 100644 index 0000000..c749688 --- /dev/null +++ b/SecretAPI/Extensions/Pickups/PickupExtensions.MicroHID.cs @@ -0,0 +1,46 @@ +namespace SecretAPI.Extensions.Pickups; + +using InventorySystem.Items.MicroHID.Modules; +using LabApi.Features.Wrappers; + +/// +/// Extensions for Micro H.I.D. +/// +public static partial class PickupExtensions +{ + extension(MicroHIDPickup pickup) + { + /// + /// Modify the energy of this . + /// + /// The energy. + /// The modified . + public MicroHIDPickup WithEnergy(float energy) + { + pickup.Energy = energy; + return pickup; + } + + /// + /// Modify the phase of this . + /// + /// The phase. + /// The modified . + public MicroHIDPickup WithPhase(MicroHidPhase phase) + { + pickup.Phase = phase; + return pickup; + } + + /// + /// Modify the firing mode of this . + /// + /// The firing mode. + /// The modified . + public MicroHIDPickup WithFiringMode(MicroHidFiringMode mode) + { + pickup.FiringMode = mode; + return pickup; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Pickups/PickupExtensions.Radio.cs b/SecretAPI/Extensions/Pickups/PickupExtensions.Radio.cs new file mode 100644 index 0000000..f45b142 --- /dev/null +++ b/SecretAPI/Extensions/Pickups/PickupExtensions.Radio.cs @@ -0,0 +1,46 @@ +namespace SecretAPI.Extensions.Pickups; + +using InventorySystem.Items.Radio; +using RadioPickup = LabApi.Features.Wrappers.RadioPickup; + +/// +/// Extensions for radios. +/// +public static partial class PickupExtensions +{ + extension(RadioPickup pickup) + { + /// + /// Modifies the enabled state of this . + /// + /// The enabled state. + /// The modified . + public RadioPickup WithEnabledState(bool enabled) + { + pickup.IsEnabled = enabled; + return pickup; + } + + /// + /// Modifies the range level of this . + /// + /// The range. + /// The modified . + public RadioPickup WithRangeLevel(RadioMessages.RadioRangeLevel range) + { + pickup.RangeLevel = range; + return pickup; + } + + /// + /// Modifies the battery of this . + /// + /// The battery. + /// The modified . + public RadioPickup WithBattery(float battery) + { + pickup.Battery = battery; + return pickup; + } + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/Pickups/PickupExtensions.cs b/SecretAPI/Extensions/Pickups/PickupExtensions.cs new file mode 100644 index 0000000..0d530b3 --- /dev/null +++ b/SecretAPI/Extensions/Pickups/PickupExtensions.cs @@ -0,0 +1,61 @@ +namespace SecretAPI.Extensions.Pickups; + +using InventorySystem.Items.Usables.Scp330; +using LabApi.Features.Wrappers; +using Scp330Pickup = LabApi.Features.Wrappers.Scp330Pickup; + +/// +/// Extensions for pickups. +/// +public static partial class PickupExtensions +{ + extension(T pickup) + where T : Pickup + { + /// + /// Modifies the weight of this . + /// + /// The weight. + /// The modified . + public T WithWeight(float weight) + { + pickup.Weight = weight; + return pickup; + } + + /// + /// Modifies the locked state of this . + /// + /// The lock state. + /// The modified . + public T WithLockedState(bool locked) + { + pickup.IsLocked = locked; + return pickup; + } + } + + /// + /// Modifies the ammo in this . + /// + /// The pickup. + /// The ammo. + /// The modified . + public static AmmoPickup WithAmmo(this AmmoPickup pickup, ushort ammo) + { + pickup.Ammo = ammo; + return pickup; + } + + /// + /// Modifies the exposed candy in this . + /// + /// The pickup. + /// The candy kind. + /// The modified . + public static Scp330Pickup WithExposedCandy(this Scp330Pickup pickup, CandyKindID kind) + { + pickup.ExposedCandy = kind; + return pickup; + } +} \ No newline at end of file diff --git a/SecretAPI/Extensions/ReflectionExtensions.cs b/SecretAPI/Extensions/ReflectionExtensions.cs index 32767cf..340cf68 100644 --- a/SecretAPI/Extensions/ReflectionExtensions.cs +++ b/SecretAPI/Extensions/ReflectionExtensions.cs @@ -11,6 +11,28 @@ /// public static class ReflectionExtensions { + /// + /// Casts an object into . + /// This will throw an exception if is not of type . + /// + /// The source object to cast from. + /// The new type to cast to. + /// The source after being cast to T. + public static T Cast(this object source) + where T : class => (T)source; + + /// + /// Casts an object of to . + /// This will require to be derived from . + /// + /// The source to cast from. + /// The original type. + /// The type to cast to. + /// The source after being cast to . + public static T2 CastTypeSafely(this T1 source) + where T1 : class + where T2 : T1 => (T2)source; + /// /// Gets the long name of a function. /// From d1b85e5f8fae14eea44ac8263e6ed60cc8e8bfd0 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 18:05:44 +0200 Subject: [PATCH 10/18] Remove unused diagnostic --- SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md | 5 ++--- SecretAPI.SourceGenerators/Diagnostics.cs | 8 -------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md index 08f562d..3d2453f 100644 --- a/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -2,6 +2,5 @@ Rule ID | Category | Severity | Notes ------------|----------|----------|--------------------- - SG001 | Usage | Error | MustBePartialPluginClass - SG002 | Usage | Error | MustBeAccessibleMethod - SG003 | Usage | Error | MustBeStaticMethod \ No newline at end of file + SG001 | Usage | Error | MustBeAccessibleMethod + SG002 | Usage | Error | MustBeStaticMethod \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs index 21d8073..a010b5f 100644 --- a/SecretAPI.SourceGenerators/Diagnostics.cs +++ b/SecretAPI.SourceGenerators/Diagnostics.cs @@ -2,14 +2,6 @@ internal static class Diagnostics { - internal static readonly DiagnosticDescriptor MustBePartialPluginClass = new( - "SG001", - "Plugin class must be partial", - "Plugin class '{0}' is missing partial modifier", - "Usage", - DiagnosticSeverity.Error, - true); - internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new( "SG002", "Method must be accessible", From 122a66fdfca792b6c4ab1938bab0eb707f0421e4 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 18:11:02 +0200 Subject: [PATCH 11/18] Fix: Diagnostic ids --- SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md | 4 +--- SecretAPI.SourceGenerators/Diagnostics.cs | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md index 60b59dd..5f28270 100644 --- a/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md +++ b/SecretAPI.SourceGenerators/AnalyzerReleases.Shipped.md @@ -1,3 +1 @@ -; Shipped analyzer releases -; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md - + \ No newline at end of file diff --git a/SecretAPI.SourceGenerators/Diagnostics.cs b/SecretAPI.SourceGenerators/Diagnostics.cs index a010b5f..8361192 100644 --- a/SecretAPI.SourceGenerators/Diagnostics.cs +++ b/SecretAPI.SourceGenerators/Diagnostics.cs @@ -3,7 +3,7 @@ internal static class Diagnostics { internal static readonly DiagnosticDescriptor MustBeAccessibleMethod = new( - "SG002", + "SG001", "Method must be accessible", "Method '{0}' has accessibility '{1}', which is not supported for generated calls", "Usage", @@ -11,7 +11,7 @@ internal static class Diagnostics true); internal static readonly DiagnosticDescriptor MustBeStaticMethod = new( - "SG003", + "SG002", "Method must be static", "Method '{0}' is not marked as static", "Usage", From ef9a1cfebcc656840704bc8ecf96b436f94a6357 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 18:49:40 +0200 Subject: [PATCH 12/18] Version bump --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 077926f..3bc488e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,7 +1,7 @@  enable - 3.1.2 + 3.2.0 From 13cb386c6de6a6af28904f4b1c8a23c07ca4189b Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 18:49:57 +0200 Subject: [PATCH 13/18] Fix: Nuget package for SourceGenerators not being correct --- .../SecretAPI.SourceGenerators.csproj | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj index b6bd4b5..ea01185 100644 --- a/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj +++ b/SecretAPI.SourceGenerators/SecretAPI.SourceGenerators.csproj @@ -7,13 +7,26 @@ enable - + + true + true + SecretAPI.SourceGenerators + Source Generators for SecretAPI. + README.md + true - false - Library - true + false + true + + + True + \ + + + + From 6bfe4aabc7a9b9f556b52004e98cf67d417816a4 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Mon, 4 May 2026 20:09:31 +0200 Subject: [PATCH 14/18] docs: Add Source Generator mention in README.md --- .github/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/README.md b/.github/README.md index 02d086c..d353b21 100644 --- a/.github/README.md +++ b/.github/README.md @@ -13,5 +13,9 @@ # Examples You can find examples inside the `SecretAPI.Examples` folder above, this contains some example settings, patches using categories and some more. +# Source Generation +- SecretAPI includes a SourceGenerator via the Nuget package ``SecretAPI.SourceGenerators`` + - This will generate ``SecretApiGenerated.cs`` which can be used with ``CallOn(Un)LoadAttribute`` + # Support * For any issues create an [Issue](https://github.com/Misfiy/SecretAPI/issues/new) or contact me on [Discord](https://discord.gg/RYzahv3vfC). \ No newline at end of file From c34a85bf25c59e3150aa34b57854c7fe0c1815a9 Mon Sep 17 00:00:00 2001 From: OMEGA3065 Date: Tue, 5 May 2026 20:41:06 +0200 Subject: [PATCH 15/18] feat: DecalHelpers which allow for spawning of Decals without the need of a ImpactEffectsModule instance. --- SecretAPI/Features/DecalHelpers.cs | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 SecretAPI/Features/DecalHelpers.cs diff --git a/SecretAPI/Features/DecalHelpers.cs b/SecretAPI/Features/DecalHelpers.cs new file mode 100644 index 0000000..2283287 --- /dev/null +++ b/SecretAPI/Features/DecalHelpers.cs @@ -0,0 +1,111 @@ +namespace SecretAPI.Features; + +using System; +using Decals; +using InventorySystem.Items; +using InventorySystem.Items.Autosync; +using InventorySystem.Items.Firearms.Modules; +using Mirror; +using RelativePositioning; +using UnityEngine; +using Utils.Networking; + +/// +/// Helps with handling Decals () and sending them to clients without the need for a firearm with the . +/// +public static class DecalHelpers +{ + private static bool hasData = false; + private static ItemType itemType = ItemType.None; + private static byte subcomponentIndex = 0; + + /// + /// Creates an for spawning a decal that can be sent to players. + /// + /// The position where the decal will spawn. + /// The position from which the decal will be applied to a surface. + /// The type of decal to spawn. + /// The message which is to be sent to players. + public static AutosyncMessage GetDecalMessage(Vector3 position, Vector3 startPosition, DecalPoolType type) + { + RelativePosition hitPoint = new(position); + RelativePosition startRaycastPoint = new(startPosition); + + (ItemType itemTypeId, byte moduleIndex) = GetItemData(); + + using NetworkWriterPooled? writer = NetworkWriterPool.Get(); + writer.WriteByte(moduleIndex); + writer.WriteSubheader(ImpactEffectsModule.RpcType.ImpactDecal); + writer.WriteByte((byte)type); + writer.WriteRelativePosition(hitPoint); + writer.WriteRelativePosition(startRaycastPoint); + + return new AutosyncMessage(writer, new ItemIdentifier(itemTypeId, 0)); + } + + /// + /// Spawns a decal. + /// + /// The position where the decal will spawn. + /// The position from which the decal will be applied to a surface. + /// The type of decal to spawn. + public static void SpawnDecalFromPosition(Vector3 position, Vector3 startPosition, DecalPoolType type = DecalPoolType.Blood) + => GetDecalMessage(position, startPosition, type).SendToAuthenticated(); + + /// + /// Spawns a decal. + /// + /// The position where the decal will spawn. + /// The normal of the face the decal is on. + /// The type of decal to spawn. + public static void SpawnDecalFromNormal(Vector3 position, Vector3 normal, DecalPoolType type = DecalPoolType.Blood) + => GetDecalMessage(position, position + normal, type).SendToAuthenticated(); + + /// + /// Spawns a decal. + /// + /// The position where the decal will spawn. + /// The normal rotation of the face the decal is on. + /// The type of decal to spawn. + public static void SpawnDecalFromNormal(Vector3 position, Quaternion normal, DecalPoolType type = DecalPoolType.Blood) + => GetDecalMessage(position, position + (normal * Vector3.forward), type).SendToAuthenticated(); + + /// + /// Spawns a decal. + /// + /// The position where the decal will spawn. + /// The direction opposite to the normal of the face the decal will be placed on. + /// The type of decal to spawn. + public static void SpawnDecalFromDirection(Vector3 position, Vector3 direction, DecalPoolType type = DecalPoolType.Blood) + => GetDecalMessage(position, position - direction, type).SendToAuthenticated(); + + /// + /// Spawns a decal. + /// + /// The position where the decal will spawn. + /// The direction opposite to the normal of the face the decal will be placed on. + /// The type of decal to spawn. + public static void SpawnDecalFromDirection(Vector3 position, Quaternion direction, DecalPoolType type = DecalPoolType.Blood) + => GetDecalMessage(position, position - (direction * Vector3.forward), type).SendToAuthenticated(); + + private static (ItemType ItemType, byte SubcomponentIndex) GetItemData() + { + if (hasData) + return (itemType, subcomponentIndex); + + foreach (ModularAutosyncItem? autoItem in ModularAutosyncItem.AllTemplates) + { + for (byte b = 0; b < autoItem.AllSubcomponents.Length; b++) + { + if (autoItem.AllSubcomponents[b] is not ImpactEffectsModule) + continue; + subcomponentIndex = b; + itemType = autoItem.ItemTypeId; + hasData = true; + return (itemType, subcomponentIndex); + } + } + + throw new InvalidOperationException("Couldn't find the `InventorySystem.Items.Firearms.Modules.ImpactEffectsModule` in the any ModularAutosyncItem!"); + } +} \ No newline at end of file From 2461876719872b2d50bf933426cb3ceb28513feb Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Tue, 5 May 2026 20:58:21 +0200 Subject: [PATCH 16/18] CodeMatcherExtensions.cs --- SecretAPI/Extensions/CodeMatcherExtensions.cs | 75 +++++++++++++++++++ SecretAPI/SecretAPI.csproj | 2 +- 2 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 SecretAPI/Extensions/CodeMatcherExtensions.cs diff --git a/SecretAPI/Extensions/CodeMatcherExtensions.cs b/SecretAPI/Extensions/CodeMatcherExtensions.cs new file mode 100644 index 0000000..12c726b --- /dev/null +++ b/SecretAPI/Extensions/CodeMatcherExtensions.cs @@ -0,0 +1,75 @@ +namespace SecretAPI.Extensions; + +using System; +using System.Linq; +using System.Reflection.Emit; +using HarmonyLib; + +/// +/// Extensions related to . +/// +public static class CodeMatcherExtensions +{ + extension(CodeMatcher matcher) + { + /// + /// Sets to the end and then backtracks X amount. + /// + /// The amount to reverse. Must be positive number. + /// The current . + public CodeMatcher EndAndBacktrack(int backtrackCount) + { + matcher.End(); + matcher.Advance(-backtrackCount); + return matcher; + } + + /// + /// Declares a local to be used of a certain . + /// + /// The of the local to declare. + /// The declared. + /// The current . + public CodeMatcher DeclareLocal(Type localType, out LocalBuilder localBuilder) + { + localBuilder = matcher.generator.DeclareLocal(localType); + return matcher; + } + + /// + /// Declares a local to be used of a certain . + /// + /// The of the local to declare. + /// The index of the local declared. + /// The current . + public CodeMatcher DeclareLocal(Type localType, out int localIndex) + { + DeclareLocal(matcher, localType, out LocalBuilder builder); + localIndex = builder.LocalIndex; + return matcher; + } + + /// + /// Gets the first label at the current position. + /// + /// The first label at the current position. + /// The current . + public CodeMatcher GetFirstLabel(out Label label) + { + label = matcher.Labels.FirstOrDefault(); + return matcher; + } + + /// + /// Gets the first label at a specific position. + /// + /// The position to get label at. + /// The label at the position. + /// The current . + public CodeMatcher GetFirstLabelAt(int position, out Label label) + { + label = matcher.codes[position].labels.FirstOrDefault(); + return matcher; + } + } +} \ No newline at end of file diff --git a/SecretAPI/SecretAPI.csproj b/SecretAPI/SecretAPI.csproj index 055b9de..db3c6ac 100644 --- a/SecretAPI/SecretAPI.csproj +++ b/SecretAPI/SecretAPI.csproj @@ -26,7 +26,7 @@ - + From d6d5778431aa64aabd7e237a1f1632f0dd43f291 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Wed, 6 May 2026 12:00:26 +0200 Subject: [PATCH 17/18] ReflectionExtensions: Cache GetLongFuncName --- SecretAPI/Extensions/ReflectionExtensions.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/SecretAPI/Extensions/ReflectionExtensions.cs b/SecretAPI/Extensions/ReflectionExtensions.cs index 340cf68..566844a 100644 --- a/SecretAPI/Extensions/ReflectionExtensions.cs +++ b/SecretAPI/Extensions/ReflectionExtensions.cs @@ -11,6 +11,8 @@ /// public static class ReflectionExtensions { + private static readonly Dictionary<(Type, MethodInfo), string> CachedLongFuncNames = new(); + /// /// Casts an object into . /// This will throw an exception if is not of type . @@ -56,7 +58,12 @@ public static string GetLongFuncName(Type type, string methodName) /// The long function name. public static string GetLongFuncName(Type type, MethodInfo method) { - return $"{method.ReturnType.FullName} {type.FullName}::{method.Name}({string.Join(",", method.GetParameters().Select(x => x.ParameterType.FullName))})"; + if (CachedLongFuncNames.TryGetValue((type, method), out string? longFunc)) + return longFunc; + + longFunc = $"{method.ReturnType.FullName} {type.FullName}::{method.Name}({string.Join(",", method.GetParameters().Select(x => x.ParameterType.FullName))})"; + CachedLongFuncNames.Add((type, method), longFunc); + return longFunc; } /// From ad3178ad714a2968cedc82b37e5dbae36a9828b2 Mon Sep 17 00:00:00 2001 From: Eve <85962933+obvEve@users.noreply.github.com> Date: Wed, 6 May 2026 17:55:08 +0200 Subject: [PATCH 18/18] fix: IsCurrentlyAccessible not being set correctly --- SecretAPI/Features/UserSettings/CustomSetting.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SecretAPI/Features/UserSettings/CustomSetting.cs b/SecretAPI/Features/UserSettings/CustomSetting.cs index 5e5f042..95e681b 100644 --- a/SecretAPI/Features/UserSettings/CustomSetting.cs +++ b/SecretAPI/Features/UserSettings/CustomSetting.cs @@ -296,7 +296,7 @@ public static void SendSettingsToPlayer(Player player, int? version = null) CustomSetting playerSpecific = EnsurePlayerSpecificSetting(player, setting); playerSpecific.PersonalizeSetting(); - setting.IsCurrentlyAccessible = true; + playerSpecific.IsCurrentlyAccessible = true; playerSettings.Add(playerSpecific); }