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