diff --git a/AddonLib.sln b/AddonLib.sln
new file mode 100644
index 0000000..7a40e3e
--- /dev/null
+++ b/AddonLib.sln
@@ -0,0 +1,37 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.14.36414.22
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AddonLib", "AddonLib\AddonLib.csproj", "{F20927BC-313D-8EE1-5263-444B92411CF3}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostApp", "HostApp\HostApp.csproj", "{B33C25BD-0E10-6867-A1D8-A08C7A680C6A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleAddon", "SampleAddon\SampleAddon.csproj", "{FC4F0E89-D513-CD24-E6A8-5E548CFF96BC}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {F20927BC-313D-8EE1-5263-444B92411CF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {F20927BC-313D-8EE1-5263-444B92411CF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {F20927BC-313D-8EE1-5263-444B92411CF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {F20927BC-313D-8EE1-5263-444B92411CF3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B33C25BD-0E10-6867-A1D8-A08C7A680C6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B33C25BD-0E10-6867-A1D8-A08C7A680C6A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B33C25BD-0E10-6867-A1D8-A08C7A680C6A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B33C25BD-0E10-6867-A1D8-A08C7A680C6A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC4F0E89-D513-CD24-E6A8-5E548CFF96BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC4F0E89-D513-CD24-E6A8-5E548CFF96BC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC4F0E89-D513-CD24-E6A8-5E548CFF96BC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC4F0E89-D513-CD24-E6A8-5E548CFF96BC}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {EC0F0193-2573-4F67-9021-1793935F7514}
+ EndGlobalSection
+EndGlobal
diff --git a/AddonLib/AddonLib.csproj b/AddonLib/AddonLib.csproj
new file mode 100644
index 0000000..f141a70
--- /dev/null
+++ b/AddonLib/AddonLib.csproj
@@ -0,0 +1,16 @@
+
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
diff --git a/AddonLib/Attributes/AddonAccessibleAttribute.cs b/AddonLib/Attributes/AddonAccessibleAttribute.cs
new file mode 100644
index 0000000..fa1e530
--- /dev/null
+++ b/AddonLib/Attributes/AddonAccessibleAttribute.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace AddonLib;
+
+public enum AddonAccess
+{
+ ReadOnly,
+ ReadWrite
+}
+
+[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
+public sealed class AddonAccessibleAttribute : Attribute
+{
+ public AddonAccess Access { get; }
+ public string? Name { get; }
+
+ public AddonAccessibleAttribute(AddonAccess access = AddonAccess.ReadOnly, string? name = null)
+ {
+ Access = access;
+ Name = name;
+ }
+}
diff --git a/AddonLib/Attributes/AddonAttribute.cs b/AddonLib/Attributes/AddonAttribute.cs
new file mode 100644
index 0000000..51374de
--- /dev/null
+++ b/AddonLib/Attributes/AddonAttribute.cs
@@ -0,0 +1,18 @@
+using System;
+
+namespace AddonLib;
+
+[AttributeUsage(AttributeTargets.Assembly, AllowMultiple = false)]
+public sealed class AddonAttribute : Attribute
+{
+ public string Name { get; }
+ public string Author { get; }
+ public string Version { get; }
+
+ public AddonAttribute(string name, string author, string version)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ Author = author ?? throw new ArgumentNullException(nameof(author));
+ Version = version ?? throw new ArgumentNullException(nameof(version));
+ }
+}
diff --git a/AddonLib/Attributes/AddonCallableAttribute.cs b/AddonLib/Attributes/AddonCallableAttribute.cs
new file mode 100644
index 0000000..724df7e
--- /dev/null
+++ b/AddonLib/Attributes/AddonCallableAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace AddonLib;
+
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)]
+public sealed class AddonCallableAttribute : Attribute
+{
+ public string? Name { get; }
+
+ public AddonCallableAttribute(string? name = null)
+ {
+ Name = name;
+ }
+}
diff --git a/AddonLib/Attributes/AddonCallbackAttribute.cs b/AddonLib/Attributes/AddonCallbackAttribute.cs
new file mode 100644
index 0000000..2652ed0
--- /dev/null
+++ b/AddonLib/Attributes/AddonCallbackAttribute.cs
@@ -0,0 +1,14 @@
+using System;
+
+namespace AddonLib;
+
+[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
+public sealed class AddonCallbackAttribute : Attribute
+{
+ public string? EventName { get; }
+
+ public AddonCallbackAttribute(string? eventName = null)
+ {
+ EventName = eventName; // if null, the method name will be used
+ }
+}
diff --git a/AddonLib/Core/AddonBase.cs b/AddonLib/Core/AddonBase.cs
new file mode 100644
index 0000000..97d29a5
--- /dev/null
+++ b/AddonLib/Core/AddonBase.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+
+namespace AddonLib;
+
+public abstract class AddonBase
+{
+ internal AddonManager? Manager { get; set; }
+
+ public IServiceProvider? Services { get; internal set; }
+ public AddonContext Context { get; internal set; } = default!;
+
+ // Lifecycle hooks
+ public virtual void OnLoaded() {}
+ public virtual void OnUnloading() {}
+
+ // Helpers available to addons
+ protected object? CallHost(string name, params object?[] args)
+ => (Manager ?? throw new InvalidOperationException("Addon is not attached to a manager")).CallHost(name, args);
+
+ protected object? GetHostValue(string name)
+ => (Manager ?? throw new InvalidOperationException("Addon is not attached to a manager")).GetHostValue(name);
+
+ protected void SetHostValue(string name, object? value)
+ => (Manager ?? throw new InvalidOperationException("Addon is not attached to a manager")).SetHostValue(name, value);
+}
+
+public sealed class AddonContext
+{
+ public required string Name { get; init; }
+ public required string Author { get; init; }
+ public required string Version { get; init; }
+ public required Assembly Assembly { get; init; }
+}
diff --git a/AddonLib/Core/AddonManager.cs b/AddonLib/Core/AddonManager.cs
new file mode 100644
index 0000000..ec173d9
--- /dev/null
+++ b/AddonLib/Core/AddonManager.cs
@@ -0,0 +1,338 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace AddonLib;
+
+public sealed class AddonManager
+{
+ private readonly List _addons = new();
+ private readonly ConcurrentDictionary> _callbacksByEvent = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _hostCallables = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _hostAccessibles = new(StringComparer.OrdinalIgnoreCase);
+ private readonly IServiceProvider? _defaultServices;
+
+ public string PluginsDirectory { get; }
+
+ // Default constructor keeps previous behavior (no auto init)
+ public AddonManager() : this(null, autoInitialize: false, services: null) { }
+
+ // New constructor with auto-initialization support
+ public AddonManager(string? pluginsDirectory, bool autoInitialize = true, IServiceProvider? services = null)
+ {
+ PluginsDirectory = ResolvePluginsDirectory(pluginsDirectory);
+ _defaultServices = services;
+ if (autoInitialize)
+ {
+ InitializePluginsDirectory(createIfMissing: true);
+ LoadAllFromPluginsDirectory();
+ }
+ }
+
+ public IReadOnlyList LoadedAddons => _addons;
+
+ public void RegisterHostApi(object host)
+ {
+ if (host == null) throw new ArgumentNullException(nameof(host));
+ var hostType = host.GetType();
+
+ // instance methods
+ foreach (var method in hostType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var callable = method.GetCustomAttribute();
+ if (callable == null) continue;
+ var name = callable.Name ?? method.Name;
+ _hostCallables[name] = new HostCallable(host, method);
+ }
+
+ // static methods on the same type (for convenience)
+ foreach (var method in hostType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var callable = method.GetCustomAttribute();
+ if (callable == null) continue;
+ var name = callable.Name ?? method.Name;
+ _hostCallables[name] = new HostCallable(null!, method);
+ }
+
+ // instance members
+ foreach (var member in hostType.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var acc = member.GetCustomAttribute();
+ if (acc == null) continue;
+ var name = acc.Name ?? member.Name;
+ _hostAccessibles[name] = new HostAccessible(host, member, acc.Access);
+ }
+
+ // static members on the same type (for convenience)
+ foreach (var member in hostType.GetMembers(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var acc = member.GetCustomAttribute();
+ if (acc == null) continue;
+ var name = acc.Name ?? member.Name;
+ _hostAccessibles[name] = new HostAccessible(null!, member, acc.Access);
+ }
+ }
+
+ public void RegisterHostApi(T host) where T : notnull => RegisterHostApi((object)host);
+
+ public void InitializePluginsDirectory(bool createIfMissing = true)
+ {
+ if (createIfMissing && !Directory.Exists(PluginsDirectory))
+ {
+ Directory.CreateDirectory(PluginsDirectory);
+ }
+ }
+
+ public IReadOnlyList LoadAllFromPluginsDirectory(SearchOption searchOption = SearchOption.TopDirectoryOnly)
+ {
+ var results = new List();
+ if (!Directory.Exists(PluginsDirectory)) return results;
+
+ foreach (var dll in Directory.EnumerateFiles(PluginsDirectory, "*.dll", searchOption))
+ {
+ try
+ {
+ var asm = Assembly.LoadFrom(dll);
+ // Attempt to load; skip if it isn't an addon
+ try
+ {
+ var loaded = LoadFrom(asm, _defaultServices);
+ results.Add(loaded);
+ }
+ catch (InvalidOperationException)
+ {
+ // Not an addon assembly or missing AddonBase impl, skip
+ }
+ }
+ catch
+ {
+ // Ignore any load errors and continue scanning
+ }
+ }
+
+ return results;
+ }
+
+ public LoadedAddon LoadFrom(string assemblyPath, IServiceProvider? services = null)
+ {
+ if (assemblyPath == null) throw new ArgumentNullException(nameof(assemblyPath));
+ var asm = Assembly.LoadFrom(assemblyPath);
+ return LoadFrom(asm, services);
+ }
+
+ public LoadedAddon LoadFrom(Assembly assembly, IServiceProvider? services = null)
+ {
+ var addonMeta = assembly.GetCustomAttribute()
+ ?? throw new InvalidOperationException("Assembly is not marked with [assembly: Addon(...)]");
+
+ var baseType = typeof(AddonBase);
+ var coreType = assembly.GetTypes().FirstOrDefault(t => !t.IsAbstract && baseType.IsAssignableFrom(t))
+ ?? throw new InvalidOperationException("No AddonBase implementation found in assembly");
+
+ var instance = (AddonBase)Activator.CreateInstance(coreType)!;
+ instance.Services = services ?? _defaultServices;
+ instance.Context = new AddonContext
+ {
+ Name = addonMeta.Name,
+ Author = addonMeta.Author,
+ Version = addonMeta.Version,
+ Assembly = assembly
+ };
+ instance.Manager = this;
+
+ var loaded = new LoadedAddon(instance);
+ _addons.Add(loaded);
+
+ // discover callbacks
+ foreach (var m in coreType.GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
+ {
+ var cb = m.GetCustomAttribute();
+ if (cb == null) continue;
+ var evt = cb.EventName ?? m.Name;
+ _callbacksByEvent.AddOrUpdate(evt, _ => new List { m }, (_, list) => { list.Add(m); return list; });
+ }
+
+ instance.OnLoaded();
+ return loaded;
+ }
+
+ public void Unload(LoadedAddon addon)
+ {
+ if (!_addons.Remove(addon)) return;
+ try { addon.Instance.OnUnloading(); } catch { /* swallow */ }
+
+ // remove callbacks for this addon
+ foreach (var kvp in _callbacksByEvent.ToArray())
+ {
+ kvp.Value.RemoveAll(m => m.DeclaringType == addon.Instance.GetType());
+ if (kvp.Value.Count == 0) _callbacksByEvent.TryRemove(kvp.Key, out _);
+ }
+ }
+
+ public void RaiseCallback(string eventName, params object?[] args)
+ {
+ if (!_callbacksByEvent.TryGetValue(eventName, out var list)) return;
+ foreach (var method in list.ToArray())
+ {
+ var addon = _addons.FirstOrDefault(a => a.Instance.GetType() == method.DeclaringType);
+ if (addon == null) continue;
+ InvokeAddonMethod(addon.Instance, method, args);
+ }
+ }
+
+ // Convenience: raise based on a caller method annotated with [AddonCallback]
+ public void RaiseCallback(MethodBase method, params object?[] args)
+ {
+ var cb = method.GetCustomAttribute();
+ if (cb == null) return;
+ var eventName = cb.EventName ?? method.Name;
+ RaiseCallback(eventName, args);
+ }
+
+ public object? CallHost(string name, params object?[] args)
+ {
+ if (!_hostCallables.TryGetValue(name, out var callable))
+ throw new KeyNotFoundException($"Host method not found: {name}");
+ return callable.Method.Invoke(callable.Target, args);
+ }
+
+ public object? GetHostValue(string name)
+ {
+ if (!_hostAccessibles.TryGetValue(name, out var access))
+ throw new KeyNotFoundException($"Host member not found: {name}");
+ return access.GetValue();
+ }
+
+ public void SetHostValue(string name, object? value)
+ {
+ if (!_hostAccessibles.TryGetValue(name, out var access))
+ throw new KeyNotFoundException($"Host member not found: {name}");
+ access.SetValue(value);
+ }
+
+ private static void InvokeAddonMethod(AddonBase instance, MethodInfo method, object?[] args)
+ {
+ var parameters = method.GetParameters();
+ var invokeArgs = new object?[parameters.Length];
+ for (int i = 0; i < parameters.Length; i++)
+ {
+ var p = parameters[i];
+ var targetType = p.ParameterType;
+
+ // Inject common services automatically
+ if (targetType == typeof(AddonContext))
+ {
+ invokeArgs[i] = instance.Context;
+ continue;
+ }
+ if (typeof(IServiceProvider).IsAssignableFrom(targetType) && instance.Services != null && targetType.IsInstanceOfType(instance.Services))
+ {
+ invokeArgs[i] = instance.Services;
+ continue;
+ }
+ if (targetType == typeof(AddonManager) && instance.Manager != null)
+ {
+ invokeArgs[i] = instance.Manager;
+ continue;
+ }
+
+ // Map positional args and attempt conversion when needed
+ if (i < args.Length && args[i] != null)
+ {
+ var supplied = args[i];
+ if (targetType.IsInstanceOfType(supplied))
+ {
+ invokeArgs[i] = supplied;
+ continue;
+ }
+
+ var underlying = Nullable.GetUnderlyingType(targetType) ?? targetType;
+ try
+ {
+ if (supplied is IConvertible && typeof(IConvertible).IsAssignableFrom(underlying))
+ {
+ invokeArgs[i] = Convert.ChangeType(supplied, underlying);
+ continue;
+ }
+ }
+ catch
+ {
+ // ignore conversion errors and fall back below
+ }
+ }
+
+ if (p.HasDefaultValue)
+ {
+ invokeArgs[i] = p.DefaultValue;
+ }
+ else
+ {
+ invokeArgs[i] = targetType.IsValueType ? Activator.CreateInstance(targetType) : null;
+ }
+ }
+ method.Invoke(instance, invokeArgs);
+ }
+
+ private static string ResolvePluginsDirectory(string? provided)
+ {
+ if (string.IsNullOrWhiteSpace(provided))
+ return Path.Combine(AppContext.BaseDirectory, "Plugins");
+ return Path.IsPathFullyQualified(provided!)
+ ? provided!
+ : Path.Combine(AppContext.BaseDirectory, provided!);
+ }
+}
+
+public sealed class LoadedAddon
+{
+ internal LoadedAddon(AddonBase instance) { Instance = instance; }
+ public AddonBase Instance { get; }
+}
+
+internal sealed record HostCallable(object Target, MethodInfo Method);
+
+internal sealed class HostAccessible
+{
+ private readonly object _target;
+ private readonly MemberInfo _member;
+ private readonly AddonAccess _access;
+
+ public HostAccessible(object target, MemberInfo member, AddonAccess access)
+ {
+ _target = target;
+ _member = member;
+ _access = access;
+ }
+
+ public object? GetValue()
+ {
+ return _member switch
+ {
+ FieldInfo f => f.GetValue(_target),
+ PropertyInfo p => p.GetValue(_target),
+ _ => throw new NotSupportedException("Unsupported member type")
+ };
+ }
+
+ public void SetValue(object? value)
+ {
+ if (_access == AddonAccess.ReadOnly)
+ throw new InvalidOperationException("Member is read-only for addons");
+
+ switch (_member)
+ {
+ case FieldInfo f:
+ f.SetValue(_target, value);
+ break;
+ case PropertyInfo p:
+ if (!p.CanWrite) throw new InvalidOperationException("Property is not writable");
+ p.SetValue(_target, value);
+ break;
+ default:
+ throw new NotSupportedException("Unsupported member type");
+ }
+ }
+}
diff --git a/AddonLib/Core/Extensions.cs b/AddonLib/Core/Extensions.cs
new file mode 100644
index 0000000..202db5a
--- /dev/null
+++ b/AddonLib/Core/Extensions.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+namespace AddonLib;
+
+public static class AddonExtensions
+{
+ // Helper for host to raise callbacks sourced from a method marked with [AddonCallback]
+ public static void RaiseAddonCallbackFrom(this AddonManager manager, MethodBase method, params object?[] args)
+ {
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+ var cb = method.GetCustomAttribute();
+ if (cb == null) return;
+ var eventName = cb.EventName ?? method.Name;
+ manager.RaiseCallback(eventName, args);
+ }
+
+ // Convenience overload: use caller member name when you intentionally rely on method name mapping
+ public static void RaiseAddonCallback(this AddonManager manager, [CallerMemberName] string? eventName = null, params object?[] args)
+ {
+ if (manager == null) throw new ArgumentNullException(nameof(manager));
+ if (string.IsNullOrEmpty(eventName)) return;
+ manager.RaiseCallback(eventName!, args);
+ }
+
+ public static bool TryGetAddonMetadata(this Assembly assembly, [MaybeNullWhen(false)] out AddonAttribute metadata)
+ {
+ metadata = assembly.GetCustomAttribute();
+ return metadata != null;
+ }
+}
diff --git a/HostApp/HostApp.csproj b/HostApp/HostApp.csproj
new file mode 100644
index 0000000..78386bd
--- /dev/null
+++ b/HostApp/HostApp.csproj
@@ -0,0 +1,11 @@
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
diff --git a/HostApp/Program.cs b/HostApp/Program.cs
new file mode 100644
index 0000000..a64a370
--- /dev/null
+++ b/HostApp/Program.cs
@@ -0,0 +1,47 @@
+using System.Reflection;
+using AddonLib;
+
+namespace HostApp;
+
+public class HostApi
+{
+ [AddonCallable]
+ public string Echo(string message) => $"Echo: {message}";
+
+ [AddonAccessible(AddonAccess.ReadWrite)]
+ public int Counter { get; set; }
+
+ [AddonCallback] // event name defaults to method name
+ public void OnTick()
+ {
+ // Raise callback with caller name, no need to specify event name
+ _manager.RaiseAddonCallback();
+ }
+
+ private readonly AddonManager _manager;
+ public HostApi(AddonManager manager) => _manager = manager;
+}
+
+internal class Program
+{
+ private static void Main(string[] args)
+ {
+ // Initialize manager with Plugins folder and autoload
+ var manager = new AddonManager("Plugins", autoInitialize: true);
+
+ // Register host API so addons can call Echo and access Counter
+ var hostApi = new HostApi(manager);
+ manager.RegisterHostApi(hostApi);
+
+ Console.WriteLine("Host initialized. Press T to tick, Q to quit.");
+ while (true)
+ {
+ var key = Console.ReadKey(intercept: true).Key;
+ if (key == ConsoleKey.Q) break;
+ if (key == ConsoleKey.T)
+ {
+ hostApi.OnTick();
+ }
+ }
+ }
+}
diff --git a/SampleAddon/AddonMain.cs b/SampleAddon/AddonMain.cs
new file mode 100644
index 0000000..7f8a384
--- /dev/null
+++ b/SampleAddon/AddonMain.cs
@@ -0,0 +1,33 @@
+using System.Reflection;
+using AddonLib;
+
+[assembly: Addon("Sample Addon", "Addon Author", "1.0.0")]
+
+namespace SampleAddon;
+
+public sealed class AddonMain : AddonBase
+{
+ // Listen for host OnTick event by method name
+ [AddonCallback]
+ private void OnTick(AddonContext ctx)
+ {
+ Console.WriteLine($"[{ctx.Name}] Tick received. Counter is {GetHostValue("Counter")}");
+
+ var response = CallHost("Echo", $"Tick at {DateTime.Now:HH:mm:ss}");
+ Console.WriteLine(response);
+
+ // Increment host Counter
+ var counter = (int?)GetHostValue("Counter") ?? 0;
+ SetHostValue("Counter", counter + 1);
+ }
+
+ public override void OnLoaded()
+ {
+ Console.WriteLine("SampleAddon loaded");
+ }
+
+ public override void OnUnloading()
+ {
+ Console.WriteLine("SampleAddon unloading");
+ }
+}
diff --git a/SampleAddon/SampleAddon.csproj b/SampleAddon/SampleAddon.csproj
new file mode 100644
index 0000000..0ff0fdb
--- /dev/null
+++ b/SampleAddon/SampleAddon.csproj
@@ -0,0 +1,11 @@
+
+
+ net8.0
+ enable
+ enable
+
+
+
+
+
+