Add project files.
This commit is contained in:
37
AddonLib.sln
Normal file
37
AddonLib.sln
Normal file
@@ -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
|
||||||
16
AddonLib/AddonLib.csproj
Normal file
16
AddonLib/AddonLib.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="HostApp\**\*.cs" />
|
||||||
|
<Compile Remove="SampleAddon\**\*.cs" />
|
||||||
|
<None Include="HostApp\**\*" />
|
||||||
|
<None Include="SampleAddon\**\*" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
22
AddonLib/Attributes/AddonAccessibleAttribute.cs
Normal file
22
AddonLib/Attributes/AddonAccessibleAttribute.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
18
AddonLib/Attributes/AddonAttribute.cs
Normal file
18
AddonLib/Attributes/AddonAttribute.cs
Normal file
@@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
AddonLib/Attributes/AddonCallableAttribute.cs
Normal file
14
AddonLib/Attributes/AddonCallableAttribute.cs
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
14
AddonLib/Attributes/AddonCallbackAttribute.cs
Normal file
14
AddonLib/Attributes/AddonCallbackAttribute.cs
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
35
AddonLib/Core/AddonBase.cs
Normal file
35
AddonLib/Core/AddonBase.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
338
AddonLib/Core/AddonManager.cs
Normal file
338
AddonLib/Core/AddonManager.cs
Normal file
@@ -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<LoadedAddon> _addons = new();
|
||||||
|
private readonly ConcurrentDictionary<string, List<MethodInfo>> _callbacksByEvent = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, HostCallable> _hostCallables = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
private readonly Dictionary<string, HostAccessible> _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<LoadedAddon> 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<AddonCallableAttribute>();
|
||||||
|
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<AddonCallableAttribute>();
|
||||||
|
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<AddonAccessibleAttribute>();
|
||||||
|
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<AddonAccessibleAttribute>();
|
||||||
|
if (acc == null) continue;
|
||||||
|
var name = acc.Name ?? member.Name;
|
||||||
|
_hostAccessibles[name] = new HostAccessible(null!, member, acc.Access);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterHostApi<T>(T host) where T : notnull => RegisterHostApi((object)host);
|
||||||
|
|
||||||
|
public void InitializePluginsDirectory(bool createIfMissing = true)
|
||||||
|
{
|
||||||
|
if (createIfMissing && !Directory.Exists(PluginsDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(PluginsDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<LoadedAddon> LoadAllFromPluginsDirectory(SearchOption searchOption = SearchOption.TopDirectoryOnly)
|
||||||
|
{
|
||||||
|
var results = new List<LoadedAddon>();
|
||||||
|
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<AddonAttribute>()
|
||||||
|
?? 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<AddonCallbackAttribute>();
|
||||||
|
if (cb == null) continue;
|
||||||
|
var evt = cb.EventName ?? m.Name;
|
||||||
|
_callbacksByEvent.AddOrUpdate(evt, _ => new List<MethodInfo> { 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<AddonCallbackAttribute>();
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
AddonLib/Core/Extensions.cs
Normal file
34
AddonLib/Core/Extensions.cs
Normal file
@@ -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<AddonCallbackAttribute>();
|
||||||
|
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<AddonAttribute>();
|
||||||
|
return metadata != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
11
HostApp/HostApp.csproj
Normal file
11
HostApp/HostApp.csproj
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AddonLib\AddonLib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
47
HostApp/Program.cs
Normal file
47
HostApp/Program.cs
Normal file
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
SampleAddon/AddonMain.cs
Normal file
33
SampleAddon/AddonMain.cs
Normal file
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
11
SampleAddon/SampleAddon.csproj
Normal file
11
SampleAddon/SampleAddon.csproj
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\AddonLib.csproj" />
|
||||||
|
<ProjectReference Include="..\AddonLib\AddonLib.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
Reference in New Issue
Block a user