diff --git a/.gitignore b/.gitignore index 14a4d8f..a7c9ae0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +artifacts/ bin/ obj/ /packages/ diff --git a/BepInEx.AutoPlugin.sln b/BepInEx.AutoPlugin.sln index 2d66936..6629a6f 100644 --- a/BepInEx.AutoPlugin.sln +++ b/BepInEx.AutoPlugin.sln @@ -1,7 +1,10 @@  Microsoft Visual Studio Solution File, Format Version 12.00 +# Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BepInEx.AutoPlugin", "BepInEx.AutoPlugin\BepInEx.AutoPlugin.csproj", "{DF01D949-82F0-4CCA-A094-903C8F30E691}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPlugin", "TestPlugin\TestPlugin.csproj", "{6C291C8C-CC69-45BE-9235-421F7B12B0B2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +15,12 @@ Global {DF01D949-82F0-4CCA-A094-903C8F30E691}.Debug|Any CPU.Build.0 = Debug|Any CPU {DF01D949-82F0-4CCA-A094-903C8F30E691}.Release|Any CPU.ActiveCfg = Release|Any CPU {DF01D949-82F0-4CCA-A094-903C8F30E691}.Release|Any CPU.Build.0 = Release|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C291C8C-CC69-45BE-9235-421F7B12B0B2}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE EndGlobalSection EndGlobal diff --git a/BepInEx.AutoPlugin/AnalyzerReleases.Shipped.md b/BepInEx.AutoPlugin/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000..60c1edf --- /dev/null +++ b/BepInEx.AutoPlugin/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/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md b/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000..f902779 --- /dev/null +++ b/BepInEx.AutoPlugin/AnalyzerReleases.Unshipped.md @@ -0,0 +1,8 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +AutoPlugin0001 | BepInEx.AutoPlugin | Warning | AutoPluginAnalyzer \ No newline at end of file diff --git a/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs new file mode 100644 index 0000000..8f8a1c3 --- /dev/null +++ b/BepInEx.AutoPlugin/Analyzers/PluginClassAnalyzer.cs @@ -0,0 +1,104 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace BepInEx.AutoPlugin.Analyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class PluginClassAnalyzer : DiagnosticAnalyzer +{ + const string Category = "BepInEx.AutoPlugin"; + + public static readonly DiagnosticDescriptor PluginClassMustBeMarkedPartial = new( + "AutoPlugin0001", + "Plugin class must be marked partial", + "Plugin class '{0}' must be marked partial for use with AutoPlugin", + Category, + DiagnosticSeverity.Warning, + isEnabledByDefault: true + ); + + public override ImmutableArray SupportedDiagnostics { get; } = + ImmutableArray.Create(PluginClassMustBeMarkedPartial); + + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterSymbolAction(AnalyzeClassSymbol, SymbolKind.NamedType); + } + + static void AnalyzeClassSymbol(SymbolAnalysisContext context) + { + var typeSymbol = (INamedTypeSymbol)context.Symbol; + + bool isNotClassOrIsPartialClass = typeSymbol.DeclaringSyntaxReferences.All( + static syntaxReference => + syntaxReference.GetSyntax() is not ClassDeclarationSyntax classSyntax + || classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword) + ); + + if (isNotClassOrIsPartialClass) + { + return; + } + + foreach (AttributeData attribute in typeSymbol.GetAttributes()) + { + var attributeClass = attribute.AttributeClass; + if (attributeClass is null) + { + continue; + } + + switch (attributeClass.Name) + { + case AutoPluginGenerator.BepInAutoPluginAttributeName: + string attributeFullName = attributeClass.ToDisplayString(); + if (attributeFullName != AutoPluginGenerator.BepInAutoPluginAttributeFullName) + { + continue; + } + break; + + case AutoPluginGenerator.PatcherAutoPluginAttributeName: + attributeFullName = attributeClass.ToDisplayString(); + if (attributeFullName != AutoPluginGenerator.PatcherAutoPluginAttributeFullName) + { + continue; + } + break; + + default: + continue; + } + + if ( + attribute.ApplicationSyntaxReference?.GetSyntax() + is not AttributeSyntax attributeSyntax + ) + { + continue; + } + + if ( + attributeSyntax.Parent is not AttributeListSyntax attributeList + || attributeList.Parent is not ClassDeclarationSyntax classDeclaration + ) + { + continue; + } + + var diagnostic = Diagnostic.Create( + PluginClassMustBeMarkedPartial, + classDeclaration.Identifier.GetLocation(), + classDeclaration.Identifier.ToString() + ); + + context.ReportDiagnostic(diagnostic); + } + } +} diff --git a/BepInEx.AutoPlugin/AutoPluginGenerator.cs b/BepInEx.AutoPlugin/AutoPluginGenerator.cs index 395f05c..1f6f1db 100644 --- a/BepInEx.AutoPlugin/AutoPluginGenerator.cs +++ b/BepInEx.AutoPlugin/AutoPluginGenerator.cs @@ -1,163 +1,326 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; +using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; -using Microsoft.CodeAnalysis.Text; -namespace BepInEx.AutoPlugin -{ - [Generator] - public class AutoPluginGenerator : ISourceGenerator - { - private const string AttributeCode = @"// -namespace BepInEx -{ - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional(""CodeGeneration"")] - internal sealed class BepInAutoPluginAttribute : System.Attribute - { - public BepInAutoPluginAttribute(string id = null, string name = null, string version = null) {} - } -} +namespace BepInEx.AutoPlugin; -namespace BepInEx.Preloader.Core.Patching +[Generator] +public sealed class AutoPluginGenerator : IIncrementalGenerator { - [System.AttributeUsage(System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] - [System.Diagnostics.Conditional(""CodeGeneration"")] - internal sealed class PatcherAutoPluginAttribute : System.Attribute - { - public PatcherAutoPluginAttribute(string id = null, string name = null, string version = null) {} - } -} -"; + public const string BepInAutoPluginAttributeName = "BepInAutoPluginAttribute"; + public const string PatcherAutoPluginAttributeName = "PatcherAutoPluginAttribute"; - private class SyntaxReceiver : ISyntaxContextReceiver - { - public List CandidateTypes { get; } = new List(); + public const string BepInAutoPluginAttributeFullName = + "BepInEx." + BepInAutoPluginAttributeName; + public const string PatcherAutoPluginAttributeFullName = + "BepInEx.Preloader.Core.Patching." + PatcherAutoPluginAttributeName; - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) - { - if (context.Node is ClassDeclarationSyntax classDeclarationSyntax && classDeclarationSyntax.AttributeLists.Any()) - { - CandidateTypes.Add(classDeclarationSyntax); - } - } - } + const string BepInPluginAttribute = "BepInEx.BepInPlugin"; + const string PatcherPluginInfoAttribute = "BepInEx.Preloader.Core.Patching.PatcherPluginInfo"; - public void Initialize(GeneratorInitializationContext context) + const string AttributeCode = $$""" + // + #nullable enable + namespace BepInEx { - context.RegisterForPostInitialization(i => i.AddSource("BepInAutoPluginAttribute", AttributeCode)); - context.RegisterForSyntaxNotifications(() => new SyntaxReceiver()); + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class {{BepInAutoPluginAttributeName}} : global::System.Attribute + { + public {{BepInAutoPluginAttributeName}}(string? id = null, string? name = null, string? version = null) {} + } } - private static string? GetAssemblyAttribute(GeneratorExecutionContext context, string name) + namespace BepInEx.Preloader.Core.Patching { - var attribute = context.Compilation.Assembly.GetAttributes().SingleOrDefault(x => x.AttributeClass?.Name == name); - return (string?)attribute?.ConstructorArguments.Single().Value; + [global::System.AttributeUsage(global::System.AttributeTargets.Class, Inherited = false, AllowMultiple = false)] + [global::System.Diagnostics.Conditional("CodeGeneration")] + internal sealed class {{PatcherAutoPluginAttributeName}} : global::System.Attribute + { + public {{PatcherAutoPluginAttributeName}}(string? id = null, string? name = null, string? version = null) {} + } } + """; - private enum AutoType + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterPostInitializationOutput(static ctx => { - Plugin, - Patcher, - } + ctx.AddSource("BepInAutoPluginAttribute.g.cs", AttributeCode); + }); - public void Execute(GeneratorExecutionContext context) - { - try + // First, collect all the data we might need + var assemblyData = context.CompilationProvider.Select( + static (compilation, _) => { - if (context.SyntaxContextReceiver is not SyntaxReceiver receiver) - return; + // This might be a little faster than + // ForAttributeWithMetadataName for what we're doing + // since the attributes should always exist. + var attributes = compilation.Assembly.GetAttributes(); - var pluginAttributeType = context.Compilation.GetTypeByMetadataName("BepInEx.BepInAutoPluginAttribute") ?? throw new NullReferenceException("Couldn't find BepInAutoPluginAttribute"); - var patcherAttributeType = context.Compilation.GetTypeByMetadataName("BepInEx.Preloader.Core.Patching.PatcherAutoPluginAttribute") ?? throw new NullReferenceException("Couldn't find PatcherAutoPluginAttribute"); + string? title = null; + string? informationalVersion = null; + string? version = null; - foreach (var classDeclarationSyntax in receiver.CandidateTypes) + for (int i = 0; i < attributes.Length; i++) { - var model = context.Compilation.GetSemanticModel(classDeclarationSyntax.SyntaxTree); - var typeSymbol = (INamedTypeSymbol)model.GetDeclaredSymbol(classDeclarationSyntax)!; + var attribute = attributes[i]; - AttributeData? attribute = null; - AutoType? type = null; + var attributeClass = attribute.AttributeClass; - foreach (var attributeData in typeSymbol.GetAttributes()) + if (attributeClass == null) { - if (attributeData.AttributeClass == null) continue; - - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, pluginAttributeType)) - { - type = AutoType.Plugin; - attribute = attributeData; - break; - } - - if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, patcherAttributeType)) - { - type = AutoType.Patcher; - attribute = attributeData; - break; - } + continue; } - if (attribute == null || type == null) - { + if (attribute.ConstructorArguments.Length != 1) continue; + + string? expectedSystemNamespace = attributeClass + .ContainingNamespace + ?.ContainingNamespace + ?.MetadataName; + + if (expectedSystemNamespace != "System") + continue; + + var expectedReflectionNamespace = attributeClass + .ContainingNamespace! + .MetadataName; + + if (expectedReflectionNamespace != "Reflection") + continue; + + var className = attributeClass.MetadataName; + + if (className == "AssemblyTitleAttribute") + { + title = attribute.ConstructorArguments[0].Value as string; } + else if (className == "AssemblyInformationalVersionAttribute") + { + informationalVersion = attribute.ConstructorArguments[0].Value as string; + } + else if (className == "AssemblyVersionAttribute") + { + version = attribute.ConstructorArguments[0].Value as string; + } + + if (title is not null && informationalVersion is not null) + { + // We did not check that version is not null because + // we'll use informationalVersion over version anyways. + break; + } + } + + return (compilation.AssemblyName, title, informationalVersion, version); + } + ); + + var references = context.CompilationProvider.Select( + static (compilation, _) => compilation.ReferencedAssemblyNames + ); + + // BepInEx 5 doesn't support plugins having version pre-release or build metadata + // such as 1.0.0-beta or 1.0.0+97ec53d20958b88581680d4d3c15ba59a8900ed5 + // so even if the user sets those in the project Version property, + // we need to strip them out. + IncrementalValueProvider isBepInEx5 = references.Select( + static (refs, _) => + { + var bepInEx = refs.FirstOrDefault(r => r.Name.Equals("BepInEx")); + return bepInEx is not null && bepInEx.Version.Major == 5; + } + ); + + IncrementalValueProvider shouldStripInformationalVersionBuildMetadata = + context.AnalyzerConfigOptionsProvider.Select( + static (options, _) => + { + var globalOptions = options.GlobalOptions; + globalOptions.TryGetValue( + "build_property.BepInAutoPluginStripBuildMetadata", + out var stripMetadata + ); + + bool shouldStripMetadata = + stripMetadata?.Equals("true", StringComparison.InvariantCultureIgnoreCase) + ?? false; + + return shouldStripMetadata; + } + ); + + // Now we have everything, combine and process the data + IncrementalValueProvider pluginProps = assemblyData + .Combine(isBepInEx5) + .Combine(shouldStripInformationalVersionBuildMetadata) + .Select( + static (items, _) => + { + var ( + ((assemblyName, title, informationalVersion, version), isBepInEx5), + shouldStripMetadata + ) = items; - var arguments = attribute.ConstructorArguments.Select(x => x.IsNull ? null : (string)x.Value!).ToArray(); - var id = arguments[0] ?? context.Compilation.AssemblyName; - var name = arguments[1] ?? GetAssemblyAttribute(context, nameof(AssemblyTitleAttribute)) ?? context.Compilation.AssemblyName; - var version = arguments[2] ?? GetAssemblyAttribute(context, nameof(AssemblyInformationalVersionAttribute)) ?? GetAssemblyAttribute(context, nameof(AssemblyVersionAttribute)); + string projectId = assemblyName ??= "unknown"; + string projectName = title ?? projectId; + string projectVersion = informationalVersion ?? version ?? "0.0.0.0"; - var attributeName = type switch + if (isBepInEx5) { - AutoType.Plugin => "BepInEx.BepInPlugin", - AutoType.Patcher => "BepInEx.Preloader.Core.Patching.PatcherPluginInfo", - _ => throw new ArgumentOutOfRangeException(), - }; - - var source = SourceText.From($@"// -namespace {typeSymbol.ContainingNamespace.ToDisplayString()} -{{ - [{attributeName}({typeSymbol.Name}.Id, ""{name}"", ""{version}"")] - public partial class {typeSymbol.Name} - {{ - /// - /// Id of the . - /// - public const string Id = ""{id}""; - - /// - /// Gets the name of the . - /// - public static string Name => ""{name}""; - - /// - /// Gets the version of the . - /// - public static string Version => ""{version}""; - }} -}} -", Encoding.UTF8); - - context.AddSource($"{typeSymbol.Name}_Auto{type}.cs", source); + projectVersion = projectVersion.Split('-', '+')[0]; + } + else if (shouldStripMetadata) + { + projectVersion = projectVersion.Split('+')[0]; + } + + return new PluginProps(projectId, projectName, projectVersion); } + ); + + IncrementalValuesProvider classesWithBepInAutoPlugin = + GetPluginClassesWithAttribute(context, BepInAutoPluginAttributeFullName); + + context.RegisterSourceOutput( + classesWithBepInAutoPlugin.Combine(pluginProps), + static (context, source) => WriteClass(context, source, BepInPluginAttribute) + ); + + IncrementalValuesProvider classesWithPatcherAutoPlugin = + GetPluginClassesWithAttribute(context, PatcherAutoPluginAttributeFullName); + + context.RegisterSourceOutput( + classesWithPatcherAutoPlugin.Combine(pluginProps), + static (context, source) => WriteClass(context, source, PatcherPluginInfoAttribute) + ); + } + + static IncrementalValuesProvider GetPluginClassesWithAttribute( + IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName + ) => + context + .SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate: static (s, _) => IsValidPluginClass(s), + transform: static (ctx, _) => ToPluginClass(ctx) + ) + .Where(x => x != default); + + static bool IsValidPluginClass(SyntaxNode node) + { + if (node is not ClassDeclarationSyntax { Parent: not TypeDeclarationSyntax } classSyntax) + { + return false; + } + + if (!classSyntax.Modifiers.Any(SyntaxKind.PartialKeyword)) + return false; + + return true; + } + + static PluginClass ToPluginClass(GeneratorAttributeSyntaxContext ctx) + { + var target = (ClassDeclarationSyntax)ctx.TargetNode; + if (ctx.SemanticModel.GetDeclaredSymbol(target) is not INamedTypeSymbol typeSymbol) + { + return default; + } + + var autoAttribute = ctx.Attributes.First(); + + if (autoAttribute is null || autoAttribute.ConstructorArguments.Length != 3) + return default; + + string? id = autoAttribute.ConstructorArguments[0].Value as string; + string? name = autoAttribute.ConstructorArguments[1].Value as string; + string? version = autoAttribute.ConstructorArguments[2].Value as string; + + var ns = typeSymbol.ContainingNamespace; + var containingNamespace = ns.IsGlobalNamespace ? null : ns.ToDisplayString(); + + return new PluginClass( + containingNamespace, + typeSymbol.Name, + new PropOverrides(id, name, version) + ); + } + + static void WriteClass( + SourceProductionContext context, + (PluginClass plugin, PluginProps pluginProps) source, + string attribute + ) + { + try + { + var (plugin, props) = source; + + StringBuilder sb = new(); + + sb.AppendLine("// "); + sb.AppendLine("#pragma warning disable"); + + PropOverrides overrides = plugin.Overrides; + string id = SymbolDisplay.FormatLiteral(overrides.Id ?? props.Id, true); + string name = SymbolDisplay.FormatLiteral(overrides.Name ?? props.Name, true); + string version = SymbolDisplay.FormatLiteral(overrides.Version ?? props.Version, true); + string pluginClass = plugin.ClassName; + + if (plugin.Namespace is not null) + { + sb.Append("namespace ").AppendLine(plugin.Namespace).AppendLine("{"); } - catch (Exception e) + + var classStr = $$""" + [global::{{attribute}}({{pluginClass}}.Id, {{name}}, {{version}})] + partial class {{pluginClass}} + { + /// + /// The Id of . + /// + public const string Id = {{id}}; + + /// + /// Gets the name of . + /// + public static string Name => {{name}}; + + /// + /// Gets the version of . + /// + public static string Version => {{version}}; + } + """; + + sb.AppendLine(classStr); + + if (plugin.Namespace is not null) { - context.ReportDiagnostic(Diagnostic.Create( - new DiagnosticDescriptor( - "ERROR", - $"An exception was thrown by the {nameof(AutoPluginGenerator)}", - $"An exception was thrown by the {nameof(AutoPluginGenerator)} generator: {e.ToString().Replace("\n", ",")}", - nameof(AutoPluginGenerator), - DiagnosticSeverity.Error, - true), - Location.None)); + sb.AppendLine("}"); } + + var fileName = $"{plugin.Namespace}.{plugin.ClassName}.AutoPlugin.g.cs"; + context.AddSource(fileName, sb.ToString()); + } + catch (Exception ex) + { + throw new Exception(ex.ToString().Replace('\n', ',')); } } + + public readonly record struct PluginClass( + string? Namespace, + string ClassName, + PropOverrides Overrides + ); + + public readonly record struct PropOverrides(string? Id, string? Name, string? Version); + + public readonly record struct PluginProps(string Id, string Name, string Version); } diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj index e544e4d..4364a15 100644 --- a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.csproj @@ -2,9 +2,6 @@ netstandard2.0 - latest - enable - embedded 1.1.0 dev @@ -18,13 +15,28 @@ https://github.com/BepInEx/BepInEx.AutoPlugin LGPL-3.0-or-later Generates BepInPlugin attribute and static plugin info for you + + true - - + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + - + diff --git a/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props new file mode 100644 index 0000000..57f4cac --- /dev/null +++ b/BepInEx.AutoPlugin/BepInEx.AutoPlugin.props @@ -0,0 +1,5 @@ + + + + + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..6a5586c --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,20 @@ + + + + latest + enable + enable + true + embedded + + + + + $([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./ + + + + true + + + diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..25c5bbd --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,6 @@ + + + + + + diff --git a/README.md b/README.md index 10422db..7973743 100644 --- a/README.md +++ b/README.md @@ -3,23 +3,75 @@ [![CI](https://github.com/BepInEx/BepInEx.AutoPlugin/workflows/CI/badge.svg)](https://github.com/BepInEx/BepInEx.AutoPlugin/actions) [![NuGet](https://img.shields.io/endpoint?color=blue&logo=NuGet&label=NuGet&url=https://shields.kzu.io/v/BepInEx.AutoPlugin?feed=nuget.bepinex.dev/v3/index.json)](https://nuget.bepinex.dev/packages/BepInEx.AutoPlugin) -Source generator that turns +BepInEx.AutoPlugin is an incremental C# source generator that takes the following properties from your project: -```cs -[BepInAutoPlugin("com.example.ExamplePlugin")] -public partial class ExamplePlugin : BaseUnityPlugin -{ -} +```xml + + com.example.ExamplePlugin + ExamplePlugin + 0.1.0 + ``` -into +And generates a partial class for your partial plugin class decorated with the `BepInAutoPluginAttribute`, decorating the generated class with the `BepInPluginAttribute` using the above properties: ```cs [BepInEx.BepInPlugin(ExamplePlugin.Id, "ExamplePlugin", "0.1.0")] -public class ExamplePlugin : BaseUnityPlugin +partial class ExamplePlugin : BaseUnityPlugin { public const string Id = "com.example.ExamplePlugin"; public static string Name => "ExamplePlugin"; public static string Version => "0.1.0"; } ``` + +A `PatcherAutoPluginAttribute` also exists for BepInEx 6 preloader patchers. + +## Usage + +Mark your plugin class partial, and decorate it with the `BepInAutoPluginAttribute`: + +```cs +[BepInAutoPlugin] +public partial class ExamplePlugin : BaseUnityPlugin +{ +} +``` + +And you're done! You don't need to manually include the `BepInPluginAttribute`, as the generated partial class includes that. + +You can also access all the properties in your code, as they are public members in your class: + +```cs +[BepInAutoPlugin] +public partial class ExamplePlugin : BaseUnityPlugin +{ + void Awake() + { + Logger.LogInfo($"Plugin {Name} version {Version} is loaded!"); + } +} +``` + +### Overriding Properties + +AutoPlugin allows overriding any of the properties with the optional attribute arguments: + +```cs +[BepInAutoPlugin(id: "com.example.MyOverrideId", name: "My Override Name", version: "1.2.3")] +public partial class ExamplePlugin : BaseUnityPlugin +{ +} +``` + +### MSBuild Configuration + +AutoPlugin allows stripping version build metadata to turn `1.0.0-beta+1234567890` into `1.0.0-beta` for the `Version` property by setting the following in your csproj: + +```xml + + true + +``` + +Note that this will do nothing on BepInEx 5 as it only accepts a version number without a pre-release version or build metadata, so it strips both. diff --git a/TestPlugin/TestPlugin.cs b/TestPlugin/TestPlugin.cs new file mode 100644 index 0000000..193d363 --- /dev/null +++ b/TestPlugin/TestPlugin.cs @@ -0,0 +1,20 @@ +using BepInEx; +using BepInEx.Preloader.Core.Patching; + +[BepInAutoPlugin] +public partial class PluginInGlobalNamespace { } + +namespace Plugin +{ + [BepInAutoPlugin(id: "my id", name: "my name", version: "my version")] + public partial class MyPluginWithOverrides { } + + // [PatcherAutoPlugin] + public partial class MyPatcherPlugin { } + + namespace Nested + { + [BepInAutoPlugin] + public partial class NestedPlugin { } + } +} diff --git a/TestPlugin/TestPlugin.csproj b/TestPlugin/TestPlugin.csproj new file mode 100644 index 0000000..cd62627 --- /dev/null +++ b/TestPlugin/TestPlugin.csproj @@ -0,0 +1,24 @@ + + + + test.TestPlugin + "Test Plugin" + 1.0.1-beta+69 + net9.0 + true + true + + + + + + + + + + +