baseline: OpenMU upstream b5a0961 (fresh source)
This commit is contained in:
69
src/PlugIns/AsyncEventHandler.cs
Normal file
69
src/PlugIns/AsyncEventHandler.cs
Normal file
@@ -0,0 +1,69 @@
|
||||
// <copyright file="AsyncEventHandler.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Event handler which is awaitable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the event args.</typeparam>
|
||||
/// <param name="eventArgs">The event arguments.</param>
|
||||
public delegate ValueTask AsyncEventHandler<in T>(T eventArgs);
|
||||
|
||||
/// <summary>
|
||||
/// Event handler without arguments which is awaitable.
|
||||
/// </summary>
|
||||
public delegate ValueTask AsyncEventHandler();
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="AsyncEventHandler"/>.
|
||||
/// </summary>
|
||||
public static class EventExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Invokes an event if any event handler is registered. If none is registered, nothing happens.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the event args.</typeparam>
|
||||
/// <param name="handler">The handler.</param>
|
||||
/// <param name="argsFactory">The arguments factory.</param>
|
||||
public static ValueTask SafeInvokeAsync<T>(this AsyncEventHandler<T>? handler, Func<T> argsFactory)
|
||||
{
|
||||
if (handler is null)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
return handler.Invoke(argsFactory());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an event if any event handler is registered. If none is registered, nothing happens.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the event args.</typeparam>
|
||||
/// <param name="handler">The handler.</param>
|
||||
/// <param name="args">The arguments.</param>
|
||||
public static ValueTask SafeInvokeAsync<T>(this AsyncEventHandler<T>? handler, T args)
|
||||
{
|
||||
if (handler is null)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
return handler.Invoke(args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invokes an event if any event handler is registered. If none is registered, nothing happens.
|
||||
/// </summary>
|
||||
/// <param name="handler">The handler.</param>
|
||||
public static ValueTask SafeInvokeAsync(this AsyncEventHandler? handler)
|
||||
{
|
||||
if (handler is null)
|
||||
{
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
return handler.Invoke();
|
||||
}
|
||||
}
|
||||
33
src/PlugIns/AsyncLockExtensions.cs
Normal file
33
src/PlugIns/AsyncLockExtensions.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
// <copyright file="AsyncLockExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Threading;
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="AsyncLock"/>.
|
||||
/// </summary>
|
||||
public static class AsyncLockExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Asynchronously acquires the lock. Returns a disposable that releases the lock when disposed.
|
||||
/// </summary>
|
||||
/// <param name="asyncLock">The asynchronous lock.</param>
|
||||
/// <param name="timeout">The timeout to take the lock.</param>
|
||||
/// <returns>A disposable that releases the lock when disposed. Null, if the lock couldn't be acquired within the timeout.</returns>
|
||||
public static async ValueTask<IDisposable?> LockAsync(this AsyncLock asyncLock, TimeSpan timeout)
|
||||
{
|
||||
using var cts = new CancellationTokenSource(timeout);
|
||||
try
|
||||
{
|
||||
return await asyncLock.LockAsync(cts.Token).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
35
src/PlugIns/CustomPlugInContainerAttribute.cs
Normal file
35
src/PlugIns/CustomPlugInContainerAttribute.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
// <copyright file="CustomPlugInContainerAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute which describes a custom plugin container interface.
|
||||
/// May be helpful for debugging and the user interface.
|
||||
/// A custom plugin container is used to manage this plugin point.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Interface)]
|
||||
public class CustomPlugInContainerAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlugInContainerAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the plugin container.</param>
|
||||
/// <param name="description">The description of the plugin container.</param>
|
||||
public CustomPlugInContainerAttribute(string name, string description)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the custom plugin container.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the custom plugin container.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
}
|
||||
137
src/PlugIns/CustomPlugInContainerBase.cs
Normal file
137
src/PlugIns/CustomPlugInContainerBase.cs
Normal file
@@ -0,0 +1,137 @@
|
||||
// <copyright file="CustomPlugInContainerBase.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
/// <summary>
|
||||
/// A base class for custom plugin containers, where <typeparamref name="TPlugIn"/> defines the common interface type of the plugin container.
|
||||
/// The custom plugin container basically collects all plugins of this type, and provides the method <see cref="GetPlugIn{T}"/> to retrieve
|
||||
/// a specific plugin interface. For a specific plugin interface, there can only be one 'effective' implementation.
|
||||
/// This base class defines abstract methods which help to select these 'effective' implementations.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plug in.</typeparam>
|
||||
/// <seealso cref="MUnique.OpenMU.PlugIns.PlugInContainerBase{TPlugIn}" />
|
||||
public abstract class CustomPlugInContainerBase<TPlugIn> : PlugInContainerBase<TPlugIn>, ICustomPlugInContainer<TPlugIn>
|
||||
where TPlugIn : class
|
||||
{
|
||||
private readonly ConcurrentDictionary<Type, TPlugIn> _currentlyEffectivePlugIns = new();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="CustomPlugInContainerBase{TPlugIn}"/> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The plugin manager which manages this instance.</param>
|
||||
protected CustomPlugInContainerBase(PlugInManager manager)
|
||||
: base(manager)
|
||||
{
|
||||
if (typeof(TPlugIn).GetCustomAttribute<CustomPlugInContainerAttribute>() is null)
|
||||
{
|
||||
throw new ArgumentException($"The specified type argument {typeof(TPlugIn)} isn't marked with the {typeof(CustomPlugInContainerAttribute)}.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public T? GetPlugIn<T>()
|
||||
where T : class, TPlugIn
|
||||
{
|
||||
if (this._currentlyEffectivePlugIns.TryGetValue(typeof(T), out var plugIn) && plugIn is T t)
|
||||
{
|
||||
return t;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes the active plugins of <typeparamref name="TPlugIn"/> in this instance.
|
||||
/// This method should be called in the constructor of derived classes.
|
||||
/// </summary>
|
||||
protected void Initialize()
|
||||
{
|
||||
foreach (var plugInType in this.Manager.GetKnownPlugInsOf<TPlugIn>().Where(this.Manager.IsPlugInActive))
|
||||
{
|
||||
if (!this._currentlyEffectivePlugIns.ContainsKey(plugInType))
|
||||
{
|
||||
this.CreatePlugInIfSuitable(plugInType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ActivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
base.ActivatePlugIn(plugIn);
|
||||
foreach (var viewInterface in GetInterfaceTypes(plugIn))
|
||||
{
|
||||
if (this._currentlyEffectivePlugIns.TryGetValue(viewInterface, out var currentEffectivePlugIn))
|
||||
{
|
||||
if (this.IsNewPlugInReplacingOld(currentEffectivePlugIn, plugIn))
|
||||
{
|
||||
this._currentlyEffectivePlugIns.TryUpdate(viewInterface, plugIn, currentEffectivePlugIn);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this._currentlyEffectivePlugIns.TryAdd(viewInterface, plugIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DeactivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
base.DeactivatePlugIn(plugIn);
|
||||
foreach (var viewInterface in GetInterfaceTypes(plugIn))
|
||||
{
|
||||
if (this._currentlyEffectivePlugIns.TryGetValue(viewInterface, out var currentEffectivePlugIn)
|
||||
&& currentEffectivePlugIn == plugIn
|
||||
&& this._currentlyEffectivePlugIns.TryRemove(viewInterface, out _))
|
||||
{
|
||||
if (this.DetermineEffectivePlugIn(viewInterface) is { } newEffectivePlugIn)
|
||||
{
|
||||
this._currentlyEffectivePlugIns.TryAdd(viewInterface, newEffectivePlugIn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the new plug in should replace the currently effective plug in.
|
||||
/// </summary>
|
||||
/// <param name="currentEffectivePlugIn">The current effective plug in.</param>
|
||||
/// <param name="activatedPlugIn">The activated plug in.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the new plug in should replace the currently effective plug in; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected abstract bool IsNewPlugInReplacingOld(TPlugIn currentEffectivePlugIn, TPlugIn activatedPlugIn);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the new effective plug in, after the previous one has been deactivated.
|
||||
/// </summary>
|
||||
/// <param name="interfaceType">The interface type of the actual plugin type.</param>
|
||||
/// <returns>The new effective plugin.</returns>
|
||||
protected abstract TPlugIn? DetermineEffectivePlugIn(Type interfaceType);
|
||||
|
||||
/// <summary>
|
||||
/// Creates the plug in if suitable for this instance.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
protected abstract void CreatePlugInIfSuitable(Type plugInType);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override void BeforeActivatePlugInType(Type plugInType)
|
||||
{
|
||||
base.BeforeActivatePlugInType(plugInType);
|
||||
|
||||
var knownPlugIn = this.FindKnownPlugin(plugInType);
|
||||
if (knownPlugIn is null)
|
||||
{
|
||||
this.CreatePlugInIfSuitable(plugInType);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerable<Type> GetInterfaceTypes(TPlugIn plugIn) => plugIn.GetType().GetInterfaces().Where(i => i.GetInterfaces().Contains(typeof(TPlugIn)));
|
||||
}
|
||||
20
src/PlugIns/ICustomPlugInContainer.cs
Normal file
20
src/PlugIns/ICustomPlugInContainer.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="ICustomPlugInContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A interface for a custom proxy object which manages <typeparamref name="TPlugIn"/>s in a custom way.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plug in.</typeparam>
|
||||
public interface ICustomPlugInContainer<in TPlugIn>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the plug in of the specified plugin interface type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The requested plug in type.</typeparam>
|
||||
/// <returns>The plug in, if available; Otherwise, <c>null</c>.</returns>
|
||||
T? GetPlugIn<T>()
|
||||
where T : class, TPlugIn;
|
||||
}
|
||||
27
src/PlugIns/IPlugInContainer.cs
Normal file
27
src/PlugIns/IPlugInContainer.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="IPlugInContainer.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// A interface a plugin container (e.g. a proxy object) which manages <typeparamref name="TPlugIn"/>s.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plug in.</typeparam>
|
||||
public interface IPlugInContainer<TPlugIn>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the active plug ins.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The active plug ins.
|
||||
/// </value>
|
||||
IEnumerable<TPlugIn> ActivePlugIns { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the plug in to the plugin point.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plug in.</param>
|
||||
/// <param name="isActive">If set to <c>true</c>, it's added as an active plugin; Otherwise, not.</param>
|
||||
void AddPlugIn(TPlugIn plugIn, bool isActive);
|
||||
}
|
||||
20
src/PlugIns/IStrategyPlugIn.cs
Normal file
20
src/PlugIns/IStrategyPlugIn.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
// <copyright file="IStrategyPlugIn.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a strategy plugin which provides a key under which the strategy is getting registered.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
public interface IStrategyPlugIn<out TKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the key under which the strategy is getting registered.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The key.
|
||||
/// </value>
|
||||
TKey Key { get; }
|
||||
}
|
||||
31
src/PlugIns/IStrategyPlugInProvider.cs
Normal file
31
src/PlugIns/IStrategyPlugInProvider.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
// <copyright file="IStrategyPlugInProvider.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a strategy plugin provider which holds/manages strategy plugins and provides them to the caller.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
||||
public interface IStrategyPlugInProvider<in TKey, out TStrategy>
|
||||
where TStrategy : class, IStrategyPlugIn<TKey>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the available <typeparamref name="TStrategy"/>s.
|
||||
/// </summary>
|
||||
IEnumerable<TStrategy> AvailableStrategies { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <typeparamref name="TStrategy"/> with the specified key.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The <typeparamref name="TStrategy"/>.
|
||||
/// </value>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The <typeparamref name="TStrategy"/> with the specified key, if available; Otherwise, <c>null</c>.</returns>
|
||||
TStrategy? this[TKey key] { get; }
|
||||
}
|
||||
30
src/PlugIns/ISupportCustomConfiguration.cs
Normal file
30
src/PlugIns/ISupportCustomConfiguration.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
// <copyright file="ISupportCustomConfiguration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a plugin which has a custom configuration, which should be saved in a <see cref="PlugInConfiguration.CustomConfiguration"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TCustomConfig">The type of the custom configuration.</typeparam>
|
||||
public interface ISupportCustomConfiguration<TCustomConfig>
|
||||
where TCustomConfig : class
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the configuration.
|
||||
/// </summary>
|
||||
TCustomConfig? Configuration { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Interface for a plugin which provides a method to create a default configuration which should be saved in <see cref="PlugInConfiguration.CustomConfiguration"/>.
|
||||
/// </summary>
|
||||
public interface ISupportDefaultCustomConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates the default configuration.
|
||||
/// </summary>
|
||||
/// <returns>The default configuration.</returns>
|
||||
object CreateDefaultConfig();
|
||||
}
|
||||
43
src/PlugIns/JsonConverterRegistry.cs
Normal file
43
src/PlugIns/JsonConverterRegistry.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
// <copyright file="JsonConverterRegistry.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a registry for globally used <see cref="JsonConverter" /> instances
|
||||
/// which can be added to <see cref="JsonSerializerOptions" /> where required.
|
||||
/// </summary>
|
||||
public static class JsonConverterRegistry
|
||||
{
|
||||
/// <summary>
|
||||
/// Backing field which stores all registered <see cref="JsonConverter" /> instances.
|
||||
/// </summary>
|
||||
private static readonly List<JsonConverter> _converters = new List<JsonConverter>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerable collection of all registered <see cref="JsonConverter" /> instances.
|
||||
/// </summary>
|
||||
public static IEnumerable<JsonConverter> Converters => _converters;
|
||||
|
||||
/// <summary>
|
||||
/// Registers the specified <paramref name="converter" /> so that it can be
|
||||
/// reused wherever custom <see cref="JsonSerializerOptions" /> are created.
|
||||
/// </summary>
|
||||
/// <param name="converter">The JSON converter to register.</param>
|
||||
public static void RegisterConverter(JsonConverter converter)
|
||||
{
|
||||
_converters.Add(converter);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all previously registered <see cref="JsonConverter" /> instances.
|
||||
/// </summary>
|
||||
public static void ClearConverters()
|
||||
{
|
||||
_converters.Clear();
|
||||
}
|
||||
}
|
||||
157
src/PlugIns/LocalizableString.cs
Normal file
157
src/PlugIns/LocalizableString.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
// <copyright file="LocalizableString.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
/// <summary>
|
||||
/// A helper class for providing a localizable string property.
|
||||
/// This class is currently compiled in both System.Web.dll and System.ComponentModel.DataAnnotations.dll.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See the internal class of the same name in System.ComponentModel.DataAnnotations for reference.
|
||||
/// </remarks>
|
||||
internal sealed class LocalizableString
|
||||
{
|
||||
private readonly string _propertyName;
|
||||
private Func<string?>? _cachedResult;
|
||||
private string? _propertyValue;
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
private Type? _resourceType;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LocalizableString"/> class.
|
||||
/// Constructs a localizable string, specifying the property name associated
|
||||
/// with this item. The <paramref name="propertyName" /> value will be used
|
||||
/// within any exceptions thrown as a result of localization failures.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">
|
||||
/// The name of the property being localized. This name
|
||||
/// will be used within exceptions thrown as a result of localization failures.
|
||||
/// </param>
|
||||
public LocalizableString(string propertyName)
|
||||
{
|
||||
this._propertyName = propertyName;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value of this localizable string. This value can be
|
||||
/// either the literal, non-localized value, or it can be a resource name
|
||||
/// found on the resource type supplied to <see cref="GetLocalizableValue" />.
|
||||
/// </summary>
|
||||
public string? Value
|
||||
{
|
||||
get => this._propertyValue;
|
||||
set
|
||||
{
|
||||
if (this._propertyValue != value)
|
||||
{
|
||||
this.ClearCache();
|
||||
this._propertyValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the resource type to be used for localization.
|
||||
/// </summary>
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
|
||||
public Type? ResourceType
|
||||
{
|
||||
get => this._resourceType;
|
||||
set
|
||||
{
|
||||
if (this._resourceType != value)
|
||||
{
|
||||
this.ClearCache();
|
||||
this._resourceType = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the potentially localized value.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If <see cref="ResourceType" /> has been specified and <see cref="Value" /> is not
|
||||
/// null, then localization will occur and the localized value will be returned.
|
||||
/// <para>
|
||||
/// If <see cref="ResourceType" /> is null then <see cref="Value" /> will be returned
|
||||
/// as a literal, non-localized string.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="System.InvalidOperationException">
|
||||
/// Thrown if localization fails. This can occur if <see cref="ResourceType" /> has been
|
||||
/// specified, <see cref="Value" /> is not null, but the resource could not be
|
||||
/// accessed. <see cref="ResourceType" /> must be a public class, and <see cref="Value" />
|
||||
/// must be the name of a public static string property that contains a getter.
|
||||
/// </exception>
|
||||
/// <returns>
|
||||
/// Returns the potentially localized value.
|
||||
/// </returns>
|
||||
public string? GetLocalizableValue()
|
||||
{
|
||||
if (this._cachedResult == null)
|
||||
{
|
||||
// If the property value is null, then just cache that value.
|
||||
// If the resource type is null, then the property value is literal, so cache it.
|
||||
if (this._propertyValue == null || this._resourceType == null)
|
||||
{
|
||||
this._cachedResult = () => this._propertyValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Get the property from the resource type for this resource key
|
||||
var property = this._resourceType.GetRuntimeProperty(this._propertyValue);
|
||||
|
||||
// We need to detect bad configurations so that we can throw exceptions accordingly
|
||||
var badlyConfigured = false;
|
||||
|
||||
// Make sure we found the property and it's the correct type, and that the type itself is public
|
||||
if (!this._resourceType.IsVisible || property == null ||
|
||||
property.PropertyType != typeof(string))
|
||||
{
|
||||
badlyConfigured = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Ensure the getter for the property is available as public static
|
||||
// TODO - check that GetMethod returns the same as old GetGetMethod()
|
||||
// in all situations regardless of modifiers
|
||||
var getter = property.GetMethod;
|
||||
if (getter == null || !(getter.IsPublic && getter.IsStatic))
|
||||
{
|
||||
badlyConfigured = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the property is not configured properly, then throw a missing member exception
|
||||
if (badlyConfigured)
|
||||
{
|
||||
var exceptionMessage = string.Format("Localization failed for property '{0}'. '{2}' not found in '{1}'.", this._propertyName, this._resourceType.FullName, this._propertyValue);
|
||||
this._cachedResult = () => throw new InvalidOperationException(exceptionMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
// We have a valid property, so cache the resource
|
||||
this._cachedResult = () => (string?)property!.GetValue(null, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the cached result
|
||||
return this._cachedResult();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears any cached values, forcing <see cref="GetLocalizableValue" /> to
|
||||
/// perform evaluation.
|
||||
/// </summary>
|
||||
private void ClearCache()
|
||||
{
|
||||
this._cachedResult = null;
|
||||
}
|
||||
}
|
||||
40
src/PlugIns/MUnique.OpenMU.PlugIns.csproj
Normal file
40
src/PlugIns/MUnique.OpenMU.PlugIns.csproj
Normal file
@@ -0,0 +1,40 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<WarningsAsErrors>nullable;CS4014;VSTHRD110;VSTHRD100</WarningsAsErrors>
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
|
||||
<GeneratePackageOnBuild>false</GeneratePackageOnBuild>
|
||||
<Authors>MUnique</Authors>
|
||||
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
|
||||
<RepositoryUrl>https://github.com/MUnique/OpenMU/tree/master/src/PlugIns</RepositoryUrl>
|
||||
<PackageProjectUrl>https://munique.net</PackageProjectUrl>
|
||||
<PackageLicenseExpression>MIT</PackageLicenseExpression>
|
||||
<PackageTags>MUnique OpenMU MUOnline PlugIns</PackageTags>
|
||||
<PackageId>MUnique.OpenMU.PlugIns</PackageId>
|
||||
<Description>
|
||||
MUnique.OpenMU.PlugIns contains all what's required to create plugins extension points and own plugins for dependent applications.
|
||||
</Description>
|
||||
<PackageVersion>0.9.9</PackageVersion>
|
||||
<Version>0.9.9</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Debug\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Debug\MUnique.OpenMU.PlugIns.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
<OutputPath>..\..\bin\Release\</OutputPath>
|
||||
<DocumentationFile>..\..\bin\Release\MUnique.OpenMU.PlugIns.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" />
|
||||
<PackageReference Include="Nito.AsyncEx.Coordination" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
12
src/PlugIns/PlugInAttribute.cs
Normal file
12
src/PlugIns/PlugInAttribute.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
// <copyright file="PlugInAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute which describes an implementation of a plugin interface.
|
||||
/// May be helpful for debugging and the user interface.
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class PlugInAttribute : Attribute;
|
||||
122
src/PlugIns/PlugInConfiguration.cs
Normal file
122
src/PlugIns/PlugInConfiguration.cs
Normal file
@@ -0,0 +1,122 @@
|
||||
// <copyright file="PlugInConfiguration.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for plugins.
|
||||
/// </summary>
|
||||
public class PlugInConfiguration : INotifyPropertyChanged
|
||||
{
|
||||
private bool _isActive;
|
||||
private string? _customConfiguration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the type identifier of the plugin.
|
||||
/// </summary>
|
||||
public Guid TypeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether the plugin is active.
|
||||
/// </summary>
|
||||
public bool IsActive
|
||||
{
|
||||
get => this._isActive;
|
||||
set
|
||||
{
|
||||
if (value == this._isActive)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._isActive = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the custom plug in source which will be compiled at run-time.
|
||||
/// </summary>
|
||||
public string? CustomPlugInSource { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the name of the external assembly which will be loaded at run-time.
|
||||
/// </summary>
|
||||
public string? ExternalAssemblyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a custom configuration.
|
||||
/// </summary>
|
||||
public string? CustomConfiguration
|
||||
{
|
||||
get => this._customConfiguration;
|
||||
set
|
||||
{
|
||||
if (value == this._customConfiguration)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this._customConfiguration = value;
|
||||
this.OnPropertyChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the (display) name of this plugin.
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public string Name
|
||||
{
|
||||
get
|
||||
{
|
||||
var plugInType = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.SelectMany(GetTypesSafely)
|
||||
.FirstOrDefault(t => t.GUID == this.TypeId);
|
||||
var plugInAttribute = plugInType?.GetCustomAttribute<DisplayAttribute>(inherit: false);
|
||||
|
||||
return plugInAttribute?.GetName() ?? this.TypeId.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
{
|
||||
return this.Name;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Triggers the <see cref="PropertyChanged"/> event.
|
||||
/// </summary>
|
||||
/// <param name="propertyName">The name of the changed property.</param>
|
||||
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
|
||||
{
|
||||
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
|
||||
}
|
||||
|
||||
private static IEnumerable<TypeInfo> GetTypesSafely(Assembly assembly)
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.DefinedTypes;
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.Where(t => t != null).Select(t => t!.GetTypeInfo());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return Enumerable.Empty<TypeInfo>();
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/PlugIns/PlugInConfigurationChangeApplier.cs
Normal file
92
src/PlugIns/PlugInConfigurationChangeApplier.cs
Normal file
@@ -0,0 +1,92 @@
|
||||
// <copyright file="PlugInConfigurationChangeApplier.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
/// <summary>
|
||||
/// Applies configuration changes to a local <see cref="PlugInManager" />.
|
||||
/// </summary>
|
||||
public static class PlugInConfigurationChangeApplier
|
||||
{
|
||||
/// <summary>
|
||||
/// Applies a changed configuration to the local <see cref="PlugInManager" />, if it is a plugin configuration.
|
||||
/// </summary>
|
||||
/// <param name="plugInManager">The plugin manager.</param>
|
||||
/// <param name="type">The changed configuration type.</param>
|
||||
/// <param name="id">The changed configuration identifier.</param>
|
||||
/// <param name="configuration">The changed configuration.</param>
|
||||
/// <returns><c>True</c>, if the change was applied; Otherwise, <c>false</c>.</returns>
|
||||
public static bool ApplyChangedConfiguration(this PlugInManager plugInManager, Type type, Guid id, object? configuration)
|
||||
{
|
||||
if (!TryGetPlugInConfiguration(type, configuration, out var plugInConfiguration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var plugInTypeId = plugInConfiguration.TypeId == Guid.Empty
|
||||
? id
|
||||
: plugInConfiguration.TypeId;
|
||||
plugInManager.ApplyChangedPlugInConfiguration(plugInTypeId, plugInConfiguration);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a removed configuration to the local <see cref="PlugInManager" />, if it is a plugin configuration.
|
||||
/// </summary>
|
||||
/// <param name="plugInManager">The plugin manager.</param>
|
||||
/// <param name="type">The removed configuration type.</param>
|
||||
/// <param name="id">The removed configuration identifier.</param>
|
||||
/// <returns><c>True</c>, if the change was applied; Otherwise, <c>false</c>.</returns>
|
||||
public static bool ApplyRemovedConfiguration(this PlugInManager plugInManager, Type type, Guid id)
|
||||
{
|
||||
if (!type.IsAssignableTo(typeof(PlugInConfiguration)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
plugInManager.DeactivatePlugIn(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyChangedPlugInConfiguration(this PlugInManager plugInManager, Guid id, PlugInConfiguration plugInConfiguration)
|
||||
{
|
||||
var currentlyActive = plugInManager.IsPlugInActive(id);
|
||||
if (currentlyActive && !plugInConfiguration.IsActive)
|
||||
{
|
||||
plugInManager.DeactivatePlugIn(id);
|
||||
}
|
||||
else if (!currentlyActive && plugInConfiguration.IsActive)
|
||||
{
|
||||
plugInManager.ActivatePlugIn(id);
|
||||
}
|
||||
else
|
||||
{
|
||||
plugInManager.ConfigurePlugIn(id, plugInConfiguration);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetPlugInConfiguration(Type type, object? configuration, out PlugInConfiguration plugInConfiguration)
|
||||
{
|
||||
if (configuration is PlugInConfiguration typedConfiguration)
|
||||
{
|
||||
plugInConfiguration = typedConfiguration;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (type.IsAssignableTo(typeof(PlugInConfiguration)) && configuration is JsonElement jsonElement)
|
||||
{
|
||||
var deserialized = jsonElement.Deserialize<PlugInConfiguration>();
|
||||
if (deserialized is not null)
|
||||
{
|
||||
plugInConfiguration = deserialized;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
plugInConfiguration = null!;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
27
src/PlugIns/PlugInConfigurationChangedEventArgs.cs
Normal file
27
src/PlugIns/PlugInConfigurationChangedEventArgs.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
// <copyright file="PlugInConfigurationChangedEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EventArgs"/> for the <see cref="PlugInManager.PlugInConfigurationChanged"/> event.
|
||||
/// </summary>
|
||||
public class PlugInConfigurationChangedEventArgs : PlugInEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlugInConfigurationChangedEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
/// <param name="configuration">The changed configuration.</param>
|
||||
public PlugInConfigurationChangedEventArgs(Type plugInType, PlugInConfiguration configuration)
|
||||
: base(plugInType)
|
||||
{
|
||||
this.Configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the changed configuration.
|
||||
/// </summary>
|
||||
public PlugInConfiguration Configuration { get; }
|
||||
}
|
||||
95
src/PlugIns/PlugInConfigurationExtensions.cs
Normal file
95
src/PlugIns/PlugInConfigurationExtensions.cs
Normal file
@@ -0,0 +1,95 @@
|
||||
// <copyright file="PlugInConfigurationExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the plugin configuration.
|
||||
/// </summary>
|
||||
public static class PlugInConfigurationExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The custom configuration type.</typeparam>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="referenceHandler">The reference handler.</param>
|
||||
/// <returns>
|
||||
/// The custom configuration as <typeparamref name="T" />.
|
||||
/// </returns>
|
||||
public static T? GetConfiguration<T>(this PlugInConfiguration configuration, ReferenceHandler? referenceHandler)
|
||||
where T : class
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration.CustomConfiguration))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T>(configuration.CustomConfiguration, CreateSerializerOptions(referenceHandler, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="configurationType">Type of the configuration.</param>
|
||||
/// <param name="referenceHandler">The reference handler.</param>
|
||||
/// <returns>
|
||||
/// The custom configuration as the given specified type.
|
||||
/// </returns>
|
||||
public static object? GetConfiguration(this PlugInConfiguration configuration, Type configurationType, ReferenceHandler? referenceHandler)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration.CustomConfiguration))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize(configuration.CustomConfiguration, configurationType, CreateSerializerOptions(referenceHandler, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The custom configuration type.</typeparam>
|
||||
/// <param name="plugInConfiguration">The plug in configuration.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="referenceHandler">The reference handler.</param>
|
||||
public static void SetConfiguration<T>(this PlugInConfiguration plugInConfiguration, T configuration, ReferenceHandler? referenceHandler)
|
||||
{
|
||||
plugInConfiguration.CustomConfiguration = JsonSerializer.Serialize(configuration, CreateSerializerOptions(referenceHandler, true));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the configuration.
|
||||
/// </summary>
|
||||
/// <param name="plugInConfiguration">The plug in configuration.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
/// <param name="referenceHandler">The reference handler.</param>
|
||||
public static void SetConfiguration(this PlugInConfiguration plugInConfiguration, object configuration, ReferenceHandler? referenceHandler)
|
||||
{
|
||||
plugInConfiguration.CustomConfiguration = JsonSerializer.Serialize(
|
||||
configuration,
|
||||
configuration.GetType(),
|
||||
CreateSerializerOptions(referenceHandler, true));
|
||||
}
|
||||
|
||||
private static JsonSerializerOptions CreateSerializerOptions(ReferenceHandler? referenceHandler, bool writeIndented)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = writeIndented,
|
||||
ReferenceHandler = referenceHandler,
|
||||
};
|
||||
|
||||
foreach (var converter in JsonConverterRegistry.Converters)
|
||||
{
|
||||
options.Converters.Add(converter);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
217
src/PlugIns/PlugInContainerBase.cs
Normal file
217
src/PlugIns/PlugInContainerBase.cs
Normal file
@@ -0,0 +1,217 @@
|
||||
// <copyright file="PlugInContainerBase.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using Nito.AsyncEx;
|
||||
|
||||
/// <summary>
|
||||
/// Base class for the implementation of plugin point proxies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It is used by the <see cref="PlugInProxyTypeGenerator"/> to implement proxies with Roslyn.
|
||||
/// </remarks>
|
||||
/// <typeparam name="TPlugIn">The type of the plug in.</typeparam>
|
||||
/// <seealso cref="IPlugInContainer{TPlugIn}" />
|
||||
public class PlugInContainerBase<TPlugIn> : IPlugInContainer<TPlugIn>
|
||||
where TPlugIn : class
|
||||
{
|
||||
private readonly IList<TPlugIn> _knownPlugIns = new List<TPlugIn>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlugInContainerBase{TPlugIn}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The plugin manager which manages this instance.</param>
|
||||
protected PlugInContainerBase(PlugInManager manager)
|
||||
{
|
||||
this.Manager = manager ?? throw new ArgumentNullException(nameof(manager));
|
||||
this.Manager.PlugInActivated += this.OnPlugInActivated;
|
||||
this.Manager.PlugInDeactivated += this.OnPlugInDeactivated;
|
||||
this.Manager.PlugInConfigurationChanged += this.OnPlugInConfigurationChanged;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
IEnumerable<TPlugIn> IPlugInContainer<TPlugIn>.ActivePlugIns => this.ActivePlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin manager which manages this instance.
|
||||
/// </summary>
|
||||
protected PlugInManager Manager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reader writer lock which is used to add and remove plugins.
|
||||
/// </summary>
|
||||
protected AsyncReaderWriterLock Lock { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the currently active plug ins.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The currently active plug ins.
|
||||
/// </value>
|
||||
protected IList<TPlugIn> ActivePlugIns { get; } = new List<TPlugIn>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the known plug ins.
|
||||
/// </summary>
|
||||
protected IEnumerable<TPlugIn> KnownPlugIns
|
||||
{
|
||||
get
|
||||
{
|
||||
using var l = this.Lock.ReaderLock();
|
||||
return this._knownPlugIns.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <exception cref="T:System.Threading.SynchronizationLockException">The current thread has not entered the lock in write mode.</exception>
|
||||
/// <exception cref="T:System.Threading.LockRecursionException">The <see cref="P:System.Threading.ReaderWriterLockSlim.RecursionPolicy"></see> property is <see cref="F:System.Threading.LockRecursionPolicy.NoRecursion"></see> and the current thread has already entered the lock in any mode. -or- The current thread has entered read mode, so trying to enter the lock in write mode would create the possibility of a deadlock. -or- The recursion number would exceed the capacity of the counter. The limit is so large that applications should never encounter it.</exception>
|
||||
public void AddPlugIn(TPlugIn plugIn, bool isActive)
|
||||
{
|
||||
using var l = this.Lock.WriterLock();
|
||||
this._knownPlugIns.Add(plugIn);
|
||||
if (isActive)
|
||||
{
|
||||
this.ActivatePlugIn(plugIn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the plug in from the <see cref="KnownPlugIns"/>.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plug in.</param>
|
||||
protected void RemovePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
using var l = this.Lock.WriterLock();
|
||||
this._knownPlugIns.Remove(plugIn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the plug in.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plug in.</param>
|
||||
protected virtual void ActivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
this.ActivePlugIns.Add(plugIn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the plug in.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plug in.</param>
|
||||
protected virtual void DeactivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
this.ActivePlugIns.Remove(plugIn);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called before the plug in of the specified type gets activated.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
protected virtual void BeforeActivatePlugInType(Type plugInType)
|
||||
{
|
||||
// can be overwritten to do additional stuff before activating the type.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Is called after the specified plug in has been deactivated.
|
||||
/// </summary>
|
||||
/// <param name="deactivatedPlugIn">The deactivated plug in.</param>
|
||||
protected virtual void AfterDeactivatePlugInType(TPlugIn deactivatedPlugIn)
|
||||
{
|
||||
// can be overwritten to do additional stuff after deactivating the plugin.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Finds the known plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
/// <returns>The known plugin, if found. Otherwise, null.</returns>
|
||||
protected TPlugIn? FindKnownPlugin(Type plugInType)
|
||||
{
|
||||
using var l = this.Lock.ReaderLock();
|
||||
return this._knownPlugIns.FirstOrDefault(p => p.GetType() == plugInType);
|
||||
}
|
||||
|
||||
private TPlugIn? FindActivePlugin(Type plugInType)
|
||||
{
|
||||
using var l = this.Lock.ReaderLock();
|
||||
return this.ActivePlugIns.FirstOrDefault(p => p.GetType() == plugInType);
|
||||
}
|
||||
|
||||
private bool IsEventRelevant(PlugInEventArgs e) => typeof(TPlugIn).IsAssignableFrom(e.PlugInType);
|
||||
|
||||
private void OnPlugInDeactivated(object? sender, PlugInEventArgs e)
|
||||
{
|
||||
if (!this.IsEventRelevant(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plugIn = this.FindActivePlugin(e.PlugInType);
|
||||
if (plugIn is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (this.Lock.WriterLock())
|
||||
{
|
||||
this.DeactivatePlugIn(plugIn);
|
||||
}
|
||||
|
||||
this.AfterDeactivatePlugInType(plugIn);
|
||||
}
|
||||
|
||||
private void OnPlugInActivated(object? sender, PlugInEventArgs e)
|
||||
{
|
||||
if (!this.IsEventRelevant(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
this.BeforeActivatePlugInType(e.PlugInType);
|
||||
|
||||
var plugIn = this.FindActivePlugin(e.PlugInType);
|
||||
if (plugIn != null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
plugIn = this.FindKnownPlugin(e.PlugInType);
|
||||
if (plugIn is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (this.Lock.WriterLock())
|
||||
{
|
||||
this.ActivatePlugIn(plugIn);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPlugInConfigurationChanged(object? sender, PlugInConfigurationChangedEventArgs e)
|
||||
{
|
||||
if (!this.IsEventRelevant(e))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var plugIn = this.FindActivePlugin(e.PlugInType) ?? this.FindKnownPlugin(e.PlugInType);
|
||||
if (plugIn is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.PlugInType.GetCustomConfigurationSupportInterfaceType()
|
||||
is { } configSupportInterface)
|
||||
{
|
||||
var configType = configSupportInterface.GenericTypeArguments[0];
|
||||
var typedCustomConfiguration = e.Configuration.GetConfiguration(configType, this.Manager.CustomConfigReferenceHandler);
|
||||
configSupportInterface
|
||||
.GetProperty(nameof(ISupportCustomConfiguration<object>.Configuration))
|
||||
?.SetMethod
|
||||
?.Invoke(plugIn, new[] { typedCustomConfiguration });
|
||||
}
|
||||
}
|
||||
}
|
||||
29
src/PlugIns/PlugInEventArgs.cs
Normal file
29
src/PlugIns/PlugInEventArgs.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
// <copyright file="PlugInEventArgs.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="EventArgs"/> regarding a plugin event.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.EventArgs" />
|
||||
public class PlugInEventArgs : EventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlugInEventArgs"/> class.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
public PlugInEventArgs(Type plugInType)
|
||||
{
|
||||
this.PlugInType = plugInType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the type of the plug in.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type of the plug in.
|
||||
/// </value>
|
||||
public Type PlugInType { get; }
|
||||
}
|
||||
613
src/PlugIns/PlugInManager.cs
Normal file
613
src/PlugIns/PlugInManager.cs
Normal file
@@ -0,0 +1,613 @@
|
||||
// <copyright file="PlugInManager.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.ComponentModel.Design;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Nito.Disposables.Internals;
|
||||
|
||||
/// <summary>
|
||||
/// The manager for plugins.
|
||||
/// </summary>
|
||||
public class PlugInManager
|
||||
{
|
||||
private readonly ILogger<PlugInManager> _logger;
|
||||
private readonly ServiceContainer _serviceContainer;
|
||||
private readonly IDictionary<Type, object> _plugInPoints = new Dictionary<Type, object>();
|
||||
private readonly IDictionary<Guid, Type> _knownPlugIns = new ConcurrentDictionary<Guid, Type>();
|
||||
private readonly ConcurrentDictionary<Type, ISet<Type>> _knownPlugInsPerInterfaceType = new();
|
||||
private readonly ConcurrentDictionary<Guid, Type> _activePlugIns = new();
|
||||
private object? _lastCreatedPlugIn;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlugInManager" /> class.
|
||||
/// </summary>
|
||||
/// <param name="configurations">The configurations.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
/// <param name="serviceProvider">The service provider.</param>
|
||||
/// <param name="customConfigReferenceHandler">The reference handler for references in custom plugin configurations.</param>
|
||||
public PlugInManager(ICollection<PlugInConfiguration>? configurations, ILoggerFactory loggerFactory, IServiceProvider? serviceProvider, ReferenceHandler? customConfigReferenceHandler)
|
||||
{
|
||||
_ = typeof(Nito.AsyncEx.AsyncReaderWriterLock); // Ensure Nito.AsyncEx.Coordination is loaded so it will be available in proxy generation.
|
||||
|
||||
this._logger = loggerFactory.CreateLogger<PlugInManager>();
|
||||
this._serviceContainer = new ServiceContainer(serviceProvider);
|
||||
this._serviceContainer.AddService(typeof(PlugInManager), this);
|
||||
this._serviceContainer.AddService(typeof(ILoggerFactory), loggerFactory);
|
||||
|
||||
this.CustomConfigReferenceHandler = customConfigReferenceHandler;
|
||||
|
||||
if (configurations is not null)
|
||||
{
|
||||
this.DiscoverAndRegisterPlugIns();
|
||||
var loadedAssemblies = new HashSet<string>();
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
this.ReadConfiguration(configuration, loadedAssemblies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin got deactivated.
|
||||
/// </summary>
|
||||
public event EventHandler<PlugInEventArgs>? PlugInDeactivated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when a plugin got activated.
|
||||
/// </summary>
|
||||
public event EventHandler<PlugInEventArgs>? PlugInActivated;
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the <see cref="PlugInConfiguration.CustomConfiguration"/> has been changed.
|
||||
/// </summary>
|
||||
public event EventHandler<PlugInConfigurationChangedEventArgs>? PlugInConfigurationChanged;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the known plugin types.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The known plugin types.
|
||||
/// </value>
|
||||
public IEnumerable<Type> KnownPlugInTypes => this._knownPlugIns.Values;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the reference handler for references in custom plugin configurations.
|
||||
/// </summary>
|
||||
public ReferenceHandler? CustomConfigReferenceHandler { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and registers all plugins of all loaded assemblies.
|
||||
/// </summary>
|
||||
public void DiscoverAndRegisterPlugIns()
|
||||
{
|
||||
var plugIns = this.DiscoverNewPlugIns();
|
||||
this.ValidateNoDuplicateGuids(plugIns);
|
||||
this.RegisterPlugIns(plugIns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and registers plugins of the specified assembly.
|
||||
/// </summary>
|
||||
/// <param name="assembly">The assembly.</param>
|
||||
public void DiscoverAndRegisterPlugIns(Assembly assembly)
|
||||
{
|
||||
var plugIns = this.DiscoverNewPlugIns(this.DiscoverPlugIns(assembly));
|
||||
this.ValidateNoDuplicateGuids(plugIns);
|
||||
this.RegisterPlugIns(plugIns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers and register plugins of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the plugins that should be discovered.</typeparam>
|
||||
public void DiscoverAndRegisterPlugInsOf<T>()
|
||||
{
|
||||
var plugIns = this.DiscoverAllPlugIns().Where(type => typeof(T).IsAssignableFrom(type));
|
||||
this.RegisterPlugIns(plugIns);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the known plugins of the given interface type.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The plugin interface type. Type parameter of a <see cref="CustomPlugInContainerBase{TPlugIn}"/>.</typeparam>
|
||||
/// <returns>The known plugins of the given interface type.</returns>
|
||||
public IEnumerable<Type> GetKnownPlugInsOf<T>()
|
||||
{
|
||||
if (this._knownPlugInsPerInterfaceType.TryGetValue(typeof(T), out var result))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
return this._knownPlugIns.Values.Where(p => typeof(T).IsAssignableFrom(p));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the active plugins of the specified type.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
||||
/// <returns>The active plugins of the specified type.</returns>
|
||||
public IEnumerable<TPlugIn> GetActivePlugInsOf<TPlugIn>()
|
||||
{
|
||||
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var point) && point is IPlugInContainer<TPlugIn> container)
|
||||
{
|
||||
return container.ActivePlugIns;
|
||||
}
|
||||
|
||||
return Enumerable.Empty<TPlugIn>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the plugin of type <typeparamref name="TPlugIn"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
||||
public void DeactivatePlugIn<TPlugIn>() => this.DeactivatePlugIn(typeof(TPlugIn));
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the plugin of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plugin.</param>
|
||||
public void DeactivatePlugIn(Type plugIn) => this.DeactivatePlugIn(plugIn.GUID);
|
||||
|
||||
/// <summary>
|
||||
/// Deactivates the plugin with the specified type id.
|
||||
/// </summary>
|
||||
/// <param name="plugInId">The plugin identifier.</param>
|
||||
public void DeactivatePlugIn(Guid plugInId)
|
||||
{
|
||||
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
||||
{
|
||||
this._activePlugIns.TryRemove(plugInId, out _);
|
||||
this.PlugInDeactivated?.Invoke(this, new PlugInEventArgs(plugInType));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Activates the plugin of type <typeparamref name="TPlugIn"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
||||
public void ActivatePlugIn<TPlugIn>() => this.ActivatePlugIn(typeof(TPlugIn));
|
||||
|
||||
/// <summary>
|
||||
/// Activates the plugin of the specified type.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plugin.</param>
|
||||
public void ActivatePlugIn(Type plugIn) => this.ActivatePlugIn(plugIn.GUID);
|
||||
|
||||
/// <summary>
|
||||
/// Activates the plugin with the specified type id.
|
||||
/// </summary>
|
||||
/// <param name="plugInId">The plugin identifier.</param>
|
||||
public void ActivatePlugIn(Guid plugInId)
|
||||
{
|
||||
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
||||
{
|
||||
this._activePlugIns.TryAdd(plugInId, plugInType);
|
||||
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(plugInType));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin point which implements <typeparamref name="TPlugIn"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
||||
/// <returns>The plugin point which implements <typeparamref name="TPlugIn"/>, if available; Otherwise, <c>null</c>.</returns>
|
||||
public TPlugIn? GetPlugInPoint<TPlugIn>()
|
||||
where TPlugIn : class
|
||||
{
|
||||
if (this._plugInPoints.TryGetValue(typeof(TPlugIn), out var obj) && obj is TPlugIn plugIn)
|
||||
{
|
||||
return plugIn;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the strategy plugin.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
||||
/// <returns>The strategy plugin.</returns>
|
||||
public IStrategyPlugInProvider<TKey, TStrategy>? GetStrategyProvider<TKey, TStrategy>()
|
||||
where TStrategy : class, IStrategyPlugIn<TKey>
|
||||
{
|
||||
if (this._plugInPoints.TryGetValue(typeof(TStrategy), out var obj) && obj is IStrategyPlugInProvider<TKey, TStrategy> plugIn)
|
||||
{
|
||||
return plugIn;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the strategy plugin.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The strategy plugin of the specified key, if available; Otherwise, <c>null</c>.</returns>
|
||||
public TStrategy? GetStrategy<TKey, TStrategy>(TKey key)
|
||||
where TStrategy : class, IStrategyPlugIn<TKey>
|
||||
{
|
||||
return this.GetStrategyProvider<TKey, TStrategy>()?[key];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the strategy plugin.
|
||||
/// </summary>
|
||||
/// <typeparam name="TStrategy">The type of the strategy.</typeparam>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <returns>The strategy plugin of the specified key, if available; Otherwise, <c>null</c>.</returns>
|
||||
public TStrategy? GetStrategy<TStrategy>(string key)
|
||||
where TStrategy : class, IStrategyPlugIn<string>
|
||||
{
|
||||
return this.GetStrategy<string, TStrategy>(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified plugin type is configured as active.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plugin.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified plugin type is configured as active; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool IsPlugInActive(Type plugInType)
|
||||
{
|
||||
return this.IsPlugInActive(plugInType.GUID);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified plugin type is configured as active.
|
||||
/// </summary>
|
||||
/// <param name="plugInTypeId">Identifier of the type of the plugin.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified plugin type is configured as active; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public bool IsPlugInActive(Guid plugInTypeId)
|
||||
{
|
||||
return this._activePlugIns.ContainsKey(plugInTypeId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the plugin class for the specified plugin interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugInInterface">The type of the plugin interface.</typeparam>
|
||||
/// <typeparam name="TPlugInClass">The type of the plugin class.</typeparam>
|
||||
public void RegisterPlugIn<TPlugInInterface, TPlugInClass>()
|
||||
where TPlugInInterface : class
|
||||
where TPlugInClass : class, TPlugInInterface
|
||||
{
|
||||
this.RegisterPlugInType(typeof(TPlugInClass));
|
||||
if (typeof(TPlugInInterface).GetCustomAttribute(typeof(PlugInPointAttribute)) != null)
|
||||
{
|
||||
var plugIn = this._lastCreatedPlugIn as TPlugInClass ?? this.CreatePlugInInstance<TPlugInClass>();
|
||||
this._lastCreatedPlugIn = plugIn;
|
||||
this.RegisterPlugInAtPlugInPoint<TPlugInInterface>(plugIn);
|
||||
}
|
||||
else if (this.GetCustomPlugInPointType(typeof(TPlugInInterface)) is { } customPlugInPointType)
|
||||
{
|
||||
if (!this._knownPlugInsPerInterfaceType.TryGetValue(customPlugInPointType, out var plugInList))
|
||||
{
|
||||
plugInList = new HashSet<Type>();
|
||||
this._knownPlugInsPerInterfaceType.TryAdd(customPlugInPointType, plugInList);
|
||||
}
|
||||
|
||||
plugInList.Add(typeof(TPlugInClass));
|
||||
this.PlugInActivated?.Invoke(this, new PlugInEventArgs(typeof(TPlugInClass)));
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("Plugin {PlugInClass} wasn't registered, because it's not an implementation of an interface which is marked with PlugInPointAttribute or CustomPlugInContainerAttribute.", typeof(TPlugInClass));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the plugin instance for the specified plugin point interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugInInterface">The type of the plugin interface.</typeparam>
|
||||
/// <param name="instance">The instance.</param>
|
||||
/// <exception cref="ArgumentException">Plugin Type {instance.GetType()} - instance.</exception>
|
||||
public void RegisterPlugInAtPlugInPoint<TPlugInInterface>(TPlugInInterface instance)
|
||||
where TPlugInInterface : class
|
||||
{
|
||||
if (instance.GetType().GetCustomAttribute<GuidAttribute>() is null)
|
||||
{
|
||||
throw new ArgumentException($"Plugin Type {instance.GetType()} is missing a {typeof(GuidAttribute)}. It's required to identify the plugin in the configuration. Otherwise, the plugin would get a different Guid every time it gets compiled.", nameof(instance));
|
||||
}
|
||||
|
||||
if (!this._plugInPoints.TryGetValue(typeof(TPlugInInterface), out var point))
|
||||
{
|
||||
var proxy = this.CreateProxy<TPlugInInterface>();
|
||||
proxy.AddPlugIn(instance, true);
|
||||
this._plugInPoints.Add(typeof(TPlugInInterface), proxy);
|
||||
}
|
||||
else if (point is IPlugInContainer<TPlugInInterface> proxy)
|
||||
{
|
||||
proxy.AddPlugIn(instance, true);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidPlugInProxyException(point.GetType(), typeof(IPlugInContainer<TPlugInInterface>));
|
||||
}
|
||||
|
||||
this.RegisterPlugInType(instance.GetType());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures the plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugInId">The plugin identifier.</param>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
public void ConfigurePlugIn(Guid plugInId, PlugInConfiguration configuration)
|
||||
{
|
||||
if (this._knownPlugIns.TryGetValue(plugInId, out var plugInType))
|
||||
{
|
||||
this.ConfigurePlugIn(plugInType, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void ValidateNoDuplicateGuids(IEnumerable<Type> plugIns)
|
||||
{
|
||||
var allPlugIns = plugIns.Concat(this._knownPlugIns.Values).Distinct();
|
||||
var duplicates = allPlugIns.GroupBy(t => t.GUID).Where(g => g.Count() > 1).ToList();
|
||||
if (duplicates.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var message = string.Join("; ", duplicates.Select(g =>
|
||||
$"{g.Key} used by {string.Join(", ", g.Select(t => t.FullName ?? t.Name))}"));
|
||||
throw new DuplicatePlugInGuidException(message);
|
||||
}
|
||||
|
||||
private Type? GetCustomPlugInPointType(Type interfaceType)
|
||||
{
|
||||
if (interfaceType.GetCustomAttribute<CustomPlugInContainerAttribute>() != null)
|
||||
{
|
||||
return interfaceType;
|
||||
}
|
||||
|
||||
return interfaceType.GetInterfaces().FirstOrDefault(i => i.GetCustomAttribute<CustomPlugInContainerAttribute>() != null);
|
||||
}
|
||||
|
||||
private void RegisterPlugInType(Type plugInType)
|
||||
{
|
||||
var plugInTypeId = plugInType.GUID;
|
||||
if (!this._knownPlugIns.ContainsKey(plugInTypeId))
|
||||
{
|
||||
this._knownPlugIns.Add(plugInTypeId, plugInType);
|
||||
this._activePlugIns.TryAdd(plugInTypeId, plugInType); // registered plugins are by default active
|
||||
this._logger.LogDebug("Added known plugin {0}, {1}", plugInTypeId, plugInType);
|
||||
}
|
||||
}
|
||||
|
||||
private IPlugInContainer<TPlugInInterface> CreateProxy<TPlugInInterface>()
|
||||
where TPlugInInterface : class
|
||||
{
|
||||
IPlugInContainer<TPlugInInterface> proxy;
|
||||
var strategyPlugInInterface = typeof(TPlugInInterface).GetInterfaces().FirstOrDefault(i => i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IStrategyPlugIn<>));
|
||||
if (strategyPlugInInterface != null)
|
||||
{
|
||||
var keyType = strategyPlugInInterface.GetGenericArguments()[0];
|
||||
var providerType = typeof(StrategyPlugInProvider<,>).MakeGenericType(keyType, typeof(TPlugInInterface));
|
||||
proxy = (IPlugInContainer<TPlugInInterface>)ActivatorUtilities.CreateInstance(this._serviceContainer, providerType);
|
||||
}
|
||||
else
|
||||
{
|
||||
var proxyGenerator = new PlugInProxyTypeGenerator();
|
||||
proxy = proxyGenerator.GenerateProxy<TPlugInInterface>(this);
|
||||
}
|
||||
|
||||
return proxy;
|
||||
}
|
||||
|
||||
private TPlugInClass CreatePlugInInstance<TPlugInClass>()
|
||||
{
|
||||
return ActivatorUtilities.CreateInstance<TPlugInClass>(this._serviceContainer);
|
||||
}
|
||||
|
||||
private void ReadConfiguration(PlugInConfiguration configuration, HashSet<string> loadedAssemblies)
|
||||
{
|
||||
if (!this._knownPlugIns.ContainsKey(configuration.TypeId))
|
||||
{
|
||||
if (!string.IsNullOrEmpty(configuration.ExternalAssemblyName)
|
||||
&& !loadedAssemblies.Contains(configuration.ExternalAssemblyName.ToLower()))
|
||||
{
|
||||
loadedAssemblies.Add(configuration.ExternalAssemblyName.ToLower());
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.LoadFile("plugins\\" + configuration.ExternalAssemblyName);
|
||||
this.DiscoverAndRegisterPlugIns(assembly);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this._logger.LogError(e, "Error while loading external plugin assembly {ExternalAssemblyName} for plugin {TypeId}.", configuration.ExternalAssemblyName, configuration.TypeId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(configuration.CustomPlugInSource))
|
||||
{
|
||||
this._logger.LogWarning("Custom plugin source found at plugin configuration: {Configuration}", configuration);
|
||||
/* TODO: Implement code signing, if we really need this feature.
|
||||
Assembly customPlugInAssembly = this.CompileCustomPlugInAssembly(configuration);
|
||||
this.DiscoverAndRegisterPlugIns(customPlugInAssembly);*/
|
||||
}
|
||||
else
|
||||
{
|
||||
// nothing we can do
|
||||
}
|
||||
}
|
||||
|
||||
if (this._knownPlugIns.TryGetValue(configuration.TypeId, out var plugInType))
|
||||
{
|
||||
if (!configuration.IsActive)
|
||||
{
|
||||
this.DeactivatePlugIn(plugInType);
|
||||
}
|
||||
|
||||
this.ConfigurePlugIn(plugInType, configuration);
|
||||
|
||||
// When the IsActive property changed, we activate/deactivate accordingly.
|
||||
// Currently, property changes are only fired for IsActive, so we don't need to check it.
|
||||
configuration.PropertyChanged += (sender, args) => this.OnConfigurationChanged(configuration, plugInType, args.PropertyName);
|
||||
}
|
||||
else
|
||||
{
|
||||
this._logger.LogWarning("Unknown plugin type for id {TypeId}", configuration.TypeId);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnConfigurationChanged(PlugInConfiguration configuration, Type plugInType, string? propertyName)
|
||||
{
|
||||
if (propertyName == nameof(PlugInConfiguration.IsActive))
|
||||
{
|
||||
if (configuration.IsActive)
|
||||
{
|
||||
this.ActivatePlugIn(plugInType);
|
||||
this.ConfigurePlugIn(plugInType, configuration);
|
||||
}
|
||||
else
|
||||
{
|
||||
this.DeactivatePlugIn(plugInType);
|
||||
}
|
||||
}
|
||||
|
||||
if (propertyName == nameof(PlugInConfiguration.CustomConfiguration))
|
||||
{
|
||||
this.ConfigurePlugIn(plugInType, configuration);
|
||||
}
|
||||
}
|
||||
|
||||
private void ConfigurePlugIn(Type plugInType, PlugInConfiguration configuration)
|
||||
{
|
||||
this.PlugInConfigurationChanged?.Invoke(this, new PlugInConfigurationChangedEventArgs(plugInType, configuration));
|
||||
}
|
||||
|
||||
private IEnumerable<Type> DiscoverNewPlugIns()
|
||||
{
|
||||
return this.DiscoverNewPlugIns(this.DiscoverAllPlugIns());
|
||||
}
|
||||
|
||||
private IEnumerable<Type> DiscoverNewPlugIns(IEnumerable<Type> allPlugIns)
|
||||
{
|
||||
var newPlugIns = allPlugIns.Where(plugIn => !this._knownPlugIns.ContainsKey(plugIn.GUID)).ToList();
|
||||
return newPlugIns;
|
||||
}
|
||||
|
||||
private IEnumerable<Type> DiscoverAllPlugIns()
|
||||
{
|
||||
return AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(assembly => assembly.FullName is not null)
|
||||
.Where(assembly => !assembly.FullName!.StartsWith("System"))
|
||||
.Where(assembly => !assembly.FullName!.StartsWith("Microsoft"))
|
||||
.Where(assembly => !assembly.FullName!.StartsWith("Nito"))
|
||||
.Where(assembly => !assembly.FullName!.StartsWith("Blazor"))
|
||||
.SelectMany(assembly =>
|
||||
{
|
||||
try
|
||||
{
|
||||
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute<PlugInAttribute>() != null);
|
||||
}
|
||||
catch (ReflectionTypeLoadException ex)
|
||||
{
|
||||
return ex.Types.WhereNotNull();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return Enumerable.Empty<Type>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private IEnumerable<Type> DiscoverPlugIns(Assembly assembly)
|
||||
{
|
||||
return assembly.DefinedTypes.Where(type => type.GetCustomAttribute<PlugInAttribute>() != null);
|
||||
}
|
||||
|
||||
private void RegisterPlugIns(IEnumerable<Type> plugIns)
|
||||
{
|
||||
foreach (var plugIn in plugIns)
|
||||
{
|
||||
try
|
||||
{
|
||||
// A plugin usually should be small, but it should be possible that one plugin can implement more than one plugin interface.
|
||||
// In this case, we need to register it at every plugin point
|
||||
var plugInInterfaces = plugIn.GetInterfaces().Where(t => t.IsInterface && (t.GetCustomAttribute<PlugInPointAttribute>() != null || t.GetCustomAttribute<CustomPlugInContainerAttribute>() != null));
|
||||
foreach (var plugInInterface in plugInInterfaces)
|
||||
{
|
||||
var genericMethod = this.GetType().GetMethods().FirstOrDefault(mi => mi.IsGenericMethod && mi.Name == nameof(this.RegisterPlugIn))?.MakeGenericMethod(plugInInterface, plugIn);
|
||||
genericMethod?.Invoke(this, []);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
this._logger.LogError(e, "Couldn't register plugin type {PlugIn}", plugIn);
|
||||
this._logger.LogError("TODO: Use ServiceContainer");
|
||||
}
|
||||
|
||||
this._lastCreatedPlugIn = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception that is thrown when two different plugin types share the same <see cref="GuidAttribute"/>.
|
||||
/// </summary>
|
||||
public class DuplicatePlugInGuidException : InvalidOperationException
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DuplicatePlugInGuidException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="message">The message.</param>
|
||||
public DuplicatePlugInGuidException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception that occurs when the created proxy doesn't implement the expected interface <see cref="IPlugInContainer{TPlugIn}"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="System.Exception" />
|
||||
public class InvalidPlugInProxyException : Exception
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="InvalidPlugInProxyException"/> class.
|
||||
/// </summary>
|
||||
/// <param name="type">The actual class type of the proxy.</param>
|
||||
/// <param name="expectedType">The expected interface type of the proxy.</param>
|
||||
public InvalidPlugInProxyException(Type type, Type expectedType)
|
||||
{
|
||||
this.Type = type;
|
||||
this.ExpectedType = expectedType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the actual class type of the proxy.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The type.
|
||||
/// </value>
|
||||
public Type Type { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the expected interface type of the proxy.
|
||||
/// </summary>
|
||||
public Type ExpectedType { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return $"Unexpected plugin proxy type {this.Type}. Expected one of {this.ExpectedType}";
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/PlugIns/PlugInPointAttribute.cs
Normal file
39
src/PlugIns/PlugInPointAttribute.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
// <copyright file="PlugInPointAttribute.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// An attribute which describes a plugin interface and its point.
|
||||
/// May be helpful for debugging and the user interface.
|
||||
/// A proxy class is automatically generated which executes all plugins which implement the marked interface.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// It's not possible to apply DisplayAttribute to interfaces.
|
||||
/// Therefore, this attribute still has <see cref="Name"/> and <see cref="Description"/> properties.
|
||||
/// </remarks>
|
||||
[AttributeUsage(AttributeTargets.Interface)]
|
||||
public class PlugInPointAttribute : Attribute
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="PlugInPointAttribute"/> class.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the plugin point.</param>
|
||||
/// <param name="description">The description of the plugin point.</param>
|
||||
public PlugInPointAttribute(string name, string description)
|
||||
{
|
||||
this.Name = name;
|
||||
this.Description = description;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the plugin point.
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the plugin point.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
}
|
||||
283
src/PlugIns/PlugInProxyTypeGenerator.cs
Normal file
283
src/PlugIns/PlugInProxyTypeGenerator.cs
Normal file
@@ -0,0 +1,283 @@
|
||||
// <copyright file="PlugInProxyTypeGenerator.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.CSharp.Syntax;
|
||||
using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory;
|
||||
|
||||
/// <summary>
|
||||
/// Generates the implementations of <see cref="IPlugInContainer{TPlugIn}"/> for specific plugin interface types.
|
||||
/// </summary>
|
||||
internal class PlugInProxyTypeGenerator
|
||||
{
|
||||
/// <summary>
|
||||
/// Generates the proxy for the given <typeparamref name="TPlugIn"/> interface.
|
||||
/// </summary>
|
||||
/// <typeparam name="TPlugIn">The type of the plug in.</typeparam>
|
||||
/// <param name="manager">The manager.</param>
|
||||
/// <returns>The generated proxy implementation.</returns>
|
||||
/// <exception cref="ArgumentException">
|
||||
/// Generic type argument {typeof(TPlugIn)}
|
||||
/// or
|
||||
/// Generic type argument {typeof(TPlugIn)} is not marked with {typeof(PlugInPointAttribute)}.
|
||||
/// </exception>
|
||||
/// <exception cref="T:System.Reflection.AmbiguousMatchException">More than one of the requested attributes was found.</exception>
|
||||
public IPlugInContainer<TPlugIn> GenerateProxy<TPlugIn>(PlugInManager manager)
|
||||
{
|
||||
var type = typeof(TPlugIn);
|
||||
if (!type.IsInterface)
|
||||
{
|
||||
throw new ArgumentException($"Generic type argument {typeof(TPlugIn)} is not an interface.");
|
||||
}
|
||||
|
||||
var attribute = type.GetCustomAttribute<PlugInPointAttribute>();
|
||||
if (attribute is null)
|
||||
{
|
||||
throw new ArgumentException($"Generic type argument {typeof(TPlugIn)} is not marked with {typeof(PlugInPointAttribute)}.");
|
||||
}
|
||||
|
||||
var syntaxFactory = CompilationUnit();
|
||||
var namespaceSyntax = NamespaceDeclaration(ParseName(this.GetType().Namespace + ".Proxies"))
|
||||
.AddUsings(
|
||||
UsingDirective(ParseName("System")),
|
||||
UsingDirective(ParseName("System.Collections.Generic")),
|
||||
UsingDirective(ParseName("System.Threading.Tasks")),
|
||||
UsingDirective(ParseName("Nito.AsyncEx")),
|
||||
UsingDirective(ParseName(typeof(TPlugIn).Namespace!)));
|
||||
var referencedNamespaces = this.GetReferencedNamespaces(type).Select(ns => UsingDirective(ParseName(ns)));
|
||||
namespaceSyntax = namespaceSyntax.AddUsings(referencedNamespaces.ToArray());
|
||||
var typeSyntax = this.ImplementProxyType<TPlugIn>(type);
|
||||
|
||||
namespaceSyntax = namespaceSyntax.AddMembers(typeSyntax);
|
||||
syntaxFactory = syntaxFactory.AddMembers(namespaceSyntax).NormalizeWhitespace();
|
||||
var proxyAssembly = syntaxFactory.SyntaxTree.CompileAndLoad(typeSyntax.Identifier.Text);
|
||||
var proxyType = proxyAssembly.GetType(namespaceSyntax.Name + "." + typeSyntax.Identifier.Text)!;
|
||||
return (IPlugInContainer<TPlugIn>)Activator.CreateInstance(proxyType, manager)!;
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetReferencedNamespaces(Type type)
|
||||
{
|
||||
return type.GetMethods()
|
||||
.SelectMany(method => method.GetParameters()
|
||||
.SelectMany(p => this.GetNamespaces(p.ParameterType)))
|
||||
.Distinct();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetNamespaces(Type type)
|
||||
{
|
||||
if (type.IsArray)
|
||||
{
|
||||
foreach (var ns in this.GetNamespaces(type.GetElementType()!))
|
||||
{
|
||||
yield return ns;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(type.Namespace))
|
||||
{
|
||||
yield return type.Namespace;
|
||||
}
|
||||
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
foreach (var arg in type.GetGenericArguments())
|
||||
{
|
||||
foreach (var ns in this.GetNamespaces(arg))
|
||||
{
|
||||
yield return ns;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private string GetTypeName(Type type)
|
||||
{
|
||||
if (type.IsGenericType)
|
||||
{
|
||||
var baseName = type.Name.Split('`')[0];
|
||||
var genericArguments = string.Join(", ", type.GetGenericArguments().Select(this.GetTypeName));
|
||||
if (type.DeclaringType != null)
|
||||
{
|
||||
return this.GetTypeName(type.DeclaringType) + "." + baseName + "<" + genericArguments + ">";
|
||||
}
|
||||
return baseName + "<" + genericArguments + ">";
|
||||
}
|
||||
|
||||
if (type.DeclaringType != null)
|
||||
{
|
||||
return this.GetTypeName(type.DeclaringType) + "." + type.Name;
|
||||
}
|
||||
|
||||
return type.Name;
|
||||
}
|
||||
|
||||
private ClassDeclarationSyntax ImplementProxyType<TPlugIn>(Type type)
|
||||
{
|
||||
var typeName = this.GetTypeName(type);
|
||||
var proxyTypeName = typeof(TPlugIn).Name.Substring(1) + "Proxy";
|
||||
var typeSyntax = ClassDeclaration(proxyTypeName)
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddBaseListTypes(
|
||||
SimpleBaseType(ParseTypeName($"{nameof(PlugInContainerBase<>)}<{typeName}>")),
|
||||
SimpleBaseType(ParseTypeName(typeName)));
|
||||
typeSyntax = typeSyntax.AddMembers(this.ImplementConstructor(proxyTypeName));
|
||||
foreach (var method in type.GetMethods().Where(m => m.ReturnType == typeof(void)))
|
||||
{
|
||||
MethodDeclarationSyntax methodDeclaration = this.ImplementMethod(type, method);
|
||||
typeSyntax = typeSyntax.AddMembers(methodDeclaration);
|
||||
}
|
||||
|
||||
foreach (var method in type.GetMethods().Where(m => m.ReturnType == typeof(ValueTask) || m.ReturnType == typeof(Task)))
|
||||
{
|
||||
MethodDeclarationSyntax methodDeclaration = this.ImplementAsyncMethod(type, method);
|
||||
typeSyntax = typeSyntax.AddMembers(methodDeclaration);
|
||||
}
|
||||
|
||||
return typeSyntax;
|
||||
}
|
||||
|
||||
private ConstructorDeclarationSyntax ImplementConstructor(string proxyTypeName)
|
||||
{
|
||||
return ConstructorDeclaration(proxyTypeName)
|
||||
.WithModifiers(TokenList(Token(SyntaxKind.PublicKeyword)))
|
||||
.WithParameterList(ParameterList(SeparatedList(
|
||||
new[]
|
||||
{
|
||||
Parameter(
|
||||
List<AttributeListSyntax>(),
|
||||
TokenList(),
|
||||
ParseTypeName(nameof(PlugInManager)),
|
||||
ParseToken("manager"),
|
||||
null),
|
||||
})))
|
||||
.WithInitializer(
|
||||
ConstructorInitializer(SyntaxKind.BaseConstructorInitializer)
|
||||
.AddArgumentListArguments(Argument(IdentifierName("manager"))))
|
||||
.WithBody(Block()); // empty body
|
||||
}
|
||||
|
||||
private MethodDeclarationSyntax ImplementAsyncMethod(Type type, MethodInfo method)
|
||||
{
|
||||
const string forEachVariableName = "plugIn";
|
||||
|
||||
var methodDeclaration = MethodDeclaration(IdentifierName(method.ReturnType.Name), method.Name)
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword))
|
||||
.AddModifiers(Token(SyntaxKind.AsyncKeyword));
|
||||
|
||||
var methodCallStatement = this.GenerateAsyncMethodCallStatement(method, forEachVariableName, out var cancelEventArgs, ref methodDeclaration);
|
||||
var methodCall = ParseStatement(methodCallStatement);
|
||||
BlockSyntax forEachBody = Block(methodCall);
|
||||
if (cancelEventArgs != null)
|
||||
{
|
||||
forEachBody = Block(IfStatement(ParseExpression("!" + cancelEventArgs.Name + ".Cancel"), forEachBody));
|
||||
}
|
||||
|
||||
BlockSyntax forEachBlock = Block(
|
||||
ForEachStatement(
|
||||
ParseTypeName(this.GetTypeName(type)),
|
||||
forEachVariableName,
|
||||
ParseExpression("this.ActivePlugIns"),
|
||||
forEachBody));
|
||||
|
||||
BlockSyntax methodBody = Block(
|
||||
ParseStatement("using var l = await this.Lock.ReaderLockAsync();"),
|
||||
forEachBlock);
|
||||
|
||||
methodDeclaration = methodDeclaration.WithBody(methodBody);
|
||||
return methodDeclaration;
|
||||
}
|
||||
|
||||
private string GenerateAsyncMethodCallStatement(MethodInfo method, string forEachVariableName, out ParameterInfo? cancelEventArgs, ref MethodDeclarationSyntax methodDeclaration)
|
||||
{
|
||||
var methodCallStatement = "await " + forEachVariableName + "." + method.Name + "(";
|
||||
bool first = true;
|
||||
cancelEventArgs = null;
|
||||
foreach (var parameter in method.GetParameters().Where(p => !string.IsNullOrWhiteSpace(p.Name)))
|
||||
{
|
||||
methodDeclaration = methodDeclaration.AddParameterListParameters(Parameter(
|
||||
List<AttributeListSyntax>(),
|
||||
TokenList(),
|
||||
ParseTypeName(this.GetTypeName(parameter.ParameterType)),
|
||||
ParseToken(parameter.Name!),
|
||||
null));
|
||||
if (!first)
|
||||
{
|
||||
methodCallStatement += ", ";
|
||||
}
|
||||
|
||||
methodCallStatement += parameter.Name;
|
||||
cancelEventArgs ??= parameter.ParameterType == typeof(CancelEventArgs) || parameter.ParameterType.IsSubclassOf(typeof(CancelEventArgs)) ? parameter : null;
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
methodCallStatement += ");";
|
||||
return methodCallStatement;
|
||||
}
|
||||
|
||||
private MethodDeclarationSyntax ImplementMethod(Type type, MethodInfo method)
|
||||
{
|
||||
const string forEachVariableName = "plugIn";
|
||||
|
||||
var methodDeclaration = MethodDeclaration(PredefinedType(Token(SyntaxKind.VoidKeyword)), method.Name)
|
||||
.AddModifiers(Token(SyntaxKind.PublicKeyword));
|
||||
|
||||
var methodCallStatement = this.GenerateSyncMethodCallStatement(method, forEachVariableName, out var cancelEventArgs, ref methodDeclaration);
|
||||
var methodCall = ParseStatement(methodCallStatement);
|
||||
BlockSyntax forEachBody = Block(methodCall);
|
||||
if (cancelEventArgs != null)
|
||||
{
|
||||
forEachBody = Block(IfStatement(ParseExpression("!" + cancelEventArgs.Name + ".Cancel"), forEachBody));
|
||||
}
|
||||
|
||||
BlockSyntax forEachBlock = Block(
|
||||
ForEachStatement(
|
||||
ParseTypeName(this.GetTypeName(type)),
|
||||
forEachVariableName,
|
||||
ParseExpression("this.ActivePlugIns"),
|
||||
forEachBody));
|
||||
|
||||
BlockSyntax methodBody = Block(
|
||||
ParseStatement("using var l = this.Lock.ReaderLock();"),
|
||||
forEachBlock);
|
||||
|
||||
methodDeclaration = methodDeclaration.WithBody(methodBody);
|
||||
return methodDeclaration;
|
||||
}
|
||||
|
||||
private string GenerateSyncMethodCallStatement(MethodInfo method, string forEachVariableName, out ParameterInfo? cancelEventArgs, ref MethodDeclarationSyntax methodDeclaration)
|
||||
{
|
||||
var methodCallStatement = forEachVariableName + "." + method.Name + "(";
|
||||
bool first = true;
|
||||
cancelEventArgs = null;
|
||||
foreach (var parameter in method.GetParameters().Where(p => !string.IsNullOrWhiteSpace(p.Name)))
|
||||
{
|
||||
methodDeclaration = methodDeclaration.AddParameterListParameters(Parameter(
|
||||
List<AttributeListSyntax>(),
|
||||
TokenList(),
|
||||
ParseTypeName(this.GetTypeName(parameter.ParameterType)),
|
||||
ParseToken(parameter.Name!),
|
||||
null));
|
||||
if (!first)
|
||||
{
|
||||
methodCallStatement += ", ";
|
||||
}
|
||||
|
||||
methodCallStatement += parameter.Name;
|
||||
cancelEventArgs ??= parameter.ParameterType == typeof(CancelEventArgs) || parameter.ParameterType.IsSubclassOf(typeof(CancelEventArgs)) ? parameter : null;
|
||||
|
||||
first = false;
|
||||
}
|
||||
|
||||
methodCallStatement += ");";
|
||||
return methodCallStatement;
|
||||
}
|
||||
}
|
||||
13
src/PlugIns/Properties/AssemblyInfo.cs
Normal file
13
src/PlugIns/Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// <copyright file="AssemblyInfo.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// General Information about an assembly is controlled through the following
|
||||
// set of attributes. Change these attribute values to modify the information
|
||||
// associated with an assembly.
|
||||
[assembly: AssemblyTitle("MUnique.OpenMU.PlugIns")]
|
||||
|
||||
[assembly: InternalsVisibleTo("MUnique.OpenMU.PlugIns.Tests")]
|
||||
245
src/PlugIns/Readme.md
Normal file
245
src/PlugIns/Readme.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# MUnique.OpenMU.PlugIns
|
||||
|
||||
This project contains the building blocks for the plugin system of OpenMU and
|
||||
doesn't have any dependencies to other OpenMU projects - so it's reuseable, if
|
||||
required.
|
||||
It also contains unit tests with almost complete test coverage, which can help
|
||||
to understand how this all works.
|
||||
|
||||
## Plugin Manager
|
||||
|
||||
A plugin manager takes care of discovering plugins and offers methods to retrieve,
|
||||
activate, deactivate and to manually register plugins.
|
||||
|
||||
## Plugin Types
|
||||
|
||||
The system supports the following kind of plugins. They are used in different
|
||||
situations.
|
||||
|
||||
### Regular Plugins
|
||||
|
||||
The plugin manager collects plugins of the same type and puts them into a
|
||||
dynamically created proxy object which iterates through all active plugins of
|
||||
the same type when a method of a plugin is executed.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
// The following call would execute the function "ExecuteSomeMethod" of
|
||||
// all active plugins which implement "ISomePlugIn":
|
||||
manager.GetPlugInPoint<ISomePlugIn>()?.ExecuteSomeMethod("example parameter");
|
||||
```
|
||||
|
||||
Note, that I use the ?. operator here, because if no plugin is defined, we might
|
||||
get null.
|
||||
|
||||
### Regular Plugins, managed by custom plugin containers
|
||||
|
||||
In this case, there is a custom plugin container which collects all plugins of
|
||||
a common plugin interface type. Additionally, there are specific plugin
|
||||
interfaces which derive from this common interface. A custom plugin container
|
||||
decides which one of the actual implementations are currently *effective*.
|
||||
|
||||
Example:
|
||||
|
||||
* We have a ```ViewPlugInContainer : ICustomPlugInContainer<IViewPlugIn>```
|
||||
which collects and manages all *IViewPlugIns*.
|
||||
Depending of the client version, it provides the plugins which fit best.
|
||||
|
||||
* We have a plugin interface ```IChatViewPlugIn : IViewPlugIn```.
|
||||
|
||||
* We have several implementations for the *IChatViewPlugIn*, e.g. for different
|
||||
client versions.
|
||||
|
||||
```csharp
|
||||
// assume that we have a manager which has already some active implementations
|
||||
// for IChatViewPlugIn available
|
||||
var container = new ViewPlugInContainer(manager);
|
||||
container.GetPlugIn<IChatViewPlugIn>()?.ShowMessage("Bob", "Hello World");
|
||||
```
|
||||
|
||||
Note, that I use the ?. operator here, because if no *IChatViewPlugIn* is active,
|
||||
we might get null.
|
||||
|
||||
### Strategy Plugins
|
||||
|
||||
Sometimes we just want to execute one specific plugin for one specific case.
|
||||
A typical example are plugins which do something for a specific chat command.
|
||||
|
||||
Example:
|
||||
```csharp
|
||||
// The following call would execute the function "HandleCommand" of
|
||||
// the active plugin which implements "IChatCommandPlugIn" and is responsible
|
||||
// for the command "/post":
|
||||
manager.GetStrategy<IChatCommandPlugIn>("/post")?.HandleCommand("/post Hello World");
|
||||
```
|
||||
|
||||
Note, that I use the ?. operator here, because if no strategy plugin is defined
|
||||
for the key, we might get null.
|
||||
|
||||
In this example, there is also some syntactic sugar used. When the key is
|
||||
something else as a string (or any non-considered type in the future), you must
|
||||
specify it's type explicitly:
|
||||
```csharp
|
||||
manager.GetStrategy<long, IAnotherStrategyPlugIn>(1337)?.DoStuff();
|
||||
```
|
||||
|
||||
## Defining Plugin Points / Interfaces
|
||||
|
||||
To define a plugin point (= interface), we simply add a new interface for it.
|
||||
|
||||
I copied this example from the unit tests:
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
|
||||
/// <summary>
|
||||
/// Example interface for a plugin.
|
||||
/// </summary>
|
||||
[Guid("34AEED37-9D62-4AE1-9320-91BB620B39C2")]
|
||||
[PlugInPoint("Example PlugIn Point", "This plugin point is an example.")]
|
||||
public interface IExamplePlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Does some stuff.
|
||||
/// </summary>
|
||||
/// <param name="player">The player.</param>
|
||||
/// <param name="text">The text.</param>
|
||||
void DoStuff(Player player, string text);
|
||||
}
|
||||
```
|
||||
|
||||
As you can see, there are two additional attributes at it:
|
||||
|
||||
* **Guid**: Every plugin interface needs a unique identifier.
|
||||
This id is compiled into ```typeof(IExamplePlugIn).GUID``` -
|
||||
when it's missing, it's just some random number. We want fixed GUID,
|
||||
so we can safely reference it later. They need to be unique for
|
||||
every interface, of course.
|
||||
|
||||
* **PlugInPoint**: This one defines the name and description and that it
|
||||
should be picked up by the plugin manager.
|
||||
|
||||
### Defining Strategy Plugins
|
||||
|
||||
The same applies here, too. We just add, that the plugin interface extends
|
||||
```IStrategyPlugIn<TKey>```.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for an example strategy plugin, where the strategy key is a string.
|
||||
/// </summary>
|
||||
[Guid("1E68B14C-9156-448A-A6AB-90E423A8E91C")]
|
||||
[PlugInPoint("Strategy Plugin Test Interface", "A strategy plugin test interface")]
|
||||
public interface IExampleStrategyPlugIn : IStrategyPlugIn<string>
|
||||
{
|
||||
/// <summary>
|
||||
/// Handles the command.
|
||||
/// </summary>
|
||||
/// <param name="command">The command.</param>
|
||||
void HandleCommand(string command);
|
||||
}
|
||||
```
|
||||
|
||||
### Defining custom plugin containers and plugins
|
||||
|
||||
This works slightly different from what you have seen above. Instead of using
|
||||
the ```PlugInPointAttribute```, the common interface has to be marked with
|
||||
the ```CustomPlugInContainerAttribute```.
|
||||
The more specialized plugin interfaces have to extend this interface.
|
||||
|
||||
I'd like to pick up the previously mentioned example:
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
/// <summary>
|
||||
/// Common interface for all plugins of a custom plugin container.
|
||||
/// </summary>
|
||||
[Guid("D6A56A13-AC5B-442B-B185-857587C59A32")]
|
||||
[CustomPlugInContainer(
|
||||
"Example Custom PlugIn Container",
|
||||
"This plugin container is an example.")]
|
||||
public interface IViewPlugIn
|
||||
{
|
||||
}
|
||||
|
||||
// we don't need additional attributes here, instead we extend IViewPlugIn.
|
||||
public interface IChatViewPlugIn : IViewPlugIn
|
||||
{
|
||||
/// <summary>
|
||||
/// Shows the message in the game client.
|
||||
/// </summary>
|
||||
/// <param name="sender">The name of the sender.</param>
|
||||
/// <param name="message">The message.</param>
|
||||
void ShowMessage(string sender, string message);
|
||||
}
|
||||
```
|
||||
|
||||
## Implementing Plugins
|
||||
|
||||
To implement the actual plugins, we just implement the previously defined interfaces.
|
||||
|
||||
Example:
|
||||
|
||||
```csharp
|
||||
using System.Runtime.InteropServices;
|
||||
using MUnique.OpenMU.GameLogic;
|
||||
using MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation of the <see cref="IExamplePlugIn"/>.
|
||||
/// </summary>
|
||||
/// <seealso cref="IExamplePlugIn" />
|
||||
[Guid("9FCA692F-2BD5-4310-8755-E20761F94180")]
|
||||
[PlugIn]
|
||||
[Display(Name = nameof(ExamplePlugIn), Description = "Just an example plugin.")]
|
||||
internal class ExamplePlugIn : IExamplePlugIn
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void DoStuff(Player player, string text)
|
||||
{
|
||||
// Stuff is done here
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Again, there are two additionally required attributes:
|
||||
|
||||
* **Guid**: Every plugin needs a unique identifier.
|
||||
This id is compiled into ```typeof(ExamplePlugIn).GUID``` -
|
||||
when it's missing, it's just some random number. We want fixed GUIDs,
|
||||
so we can safely reference it in configurations. They need to be
|
||||
unique for every implemented plugin, of course.
|
||||
|
||||
* **PlugIn**: This one defines the name and description and that it should be
|
||||
picked up by the plugin manager.
|
||||
|
||||
## Configuration
|
||||
|
||||
It's possible to initialize the plugin manager with a list of plugin configurations
|
||||
by passing them into the constructor.
|
||||
When this is done, it automatically searches for plugins in all currently
|
||||
loaded assemblies and registers them. Then it iterates through all given
|
||||
configurations, and tries to find the plugin with the corresponding id. If it
|
||||
can't find it, it looks if it's a custom/external plugin and tries to load
|
||||
their assemblies and rediscovers them.
|
||||
If it's then available, it deactivates the plugin based on the IsActive flag.
|
||||
|
||||
So, that's a simple mechanism to configure existing plugins, and to extend it
|
||||
by custom ones. Currently, there are two ways to load custom plugins:
|
||||
|
||||
* By specifying the name of an external assembly which is available in a "plugins"
|
||||
subfolder of the server.
|
||||
|
||||
* By adding the source code of the plugin into the configuration. It's compiled
|
||||
at runtime with Roslyn.
|
||||
|
||||
From a compatibility point of view, the last option is to prefer, because the
|
||||
source is always referencing the currently loaded assemblies. Compile errors
|
||||
would come up on the start of the server and could be fixed in a short time.
|
||||
108
src/PlugIns/StrategyPlugInProvider.cs
Normal file
108
src/PlugIns/StrategyPlugInProvider.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
// <copyright file="StrategyPlugInProvider.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// The implementation for the <see cref="IStrategyPlugInProvider{TKey,TPlugIn}"/> which provides plugins by their key.
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey">The type of the key.</typeparam>
|
||||
/// <typeparam name="TPlugIn">The type of the plugin.</typeparam>
|
||||
/// <seealso cref="MUnique.OpenMU.PlugIns.IStrategyPlugInProvider{TKey, TPlugIn}" />
|
||||
/// <seealso cref="IPlugInContainer{TPlugIn}" />
|
||||
public class StrategyPlugInProvider<TKey, TPlugIn> : PlugInContainerBase<TPlugIn>, IStrategyPlugInProvider<TKey, TPlugIn>
|
||||
where TPlugIn : class, IStrategyPlugIn<TKey>
|
||||
where TKey : notnull
|
||||
{
|
||||
private readonly IDictionary<TKey, TPlugIn> _effectiveStrategies = new Dictionary<TKey, TPlugIn>();
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StrategyPlugInProvider{TKey, TPlugIn}" /> class.
|
||||
/// </summary>
|
||||
/// <param name="manager">The plugin manager that manages this instance.</param>
|
||||
/// <param name="loggerFactory">The logger factory.</param>
|
||||
public StrategyPlugInProvider(PlugInManager manager, ILoggerFactory loggerFactory)
|
||||
: base(manager)
|
||||
{
|
||||
this.Logger = loggerFactory.CreateLogger(this.GetType());
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<TPlugIn> AvailableStrategies
|
||||
{
|
||||
get
|
||||
{
|
||||
using var l = this.Lock.ReaderLock();
|
||||
return this._effectiveStrategies.Values.ToList();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the logger.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The logger.
|
||||
/// </value>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public TPlugIn? this[TKey key]
|
||||
{
|
||||
get
|
||||
{
|
||||
using var l = this.Lock.ReaderLock();
|
||||
if (this.TryGetPlugIn(key, out var plugIn))
|
||||
{
|
||||
return plugIn;
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries the get the plug in with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="key">The key.</param>
|
||||
/// <param name="plugIn">The plugin.</param>
|
||||
/// <returns><c>True</c>, if the plugin has been found and returned; Otherwise, <c>false</c>.</returns>
|
||||
protected bool TryGetPlugIn(TKey key, [MaybeNullWhen(false)] out TPlugIn plugIn) => this._effectiveStrategies.TryGetValue(key, out plugIn);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void ActivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
base.ActivatePlugIn(plugIn);
|
||||
if (this._effectiveStrategies.TryGetValue(plugIn.Key, out var registeredPlugIn))
|
||||
{
|
||||
this.Logger.LogWarning($"Plugin {registeredPlugIn} with key {plugIn.Key} was already registered and is active. Plugin {plugIn} will not be effective.");
|
||||
}
|
||||
else
|
||||
{
|
||||
this.SetEffectivePlugin(plugIn);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void DeactivatePlugIn(TPlugIn plugIn)
|
||||
{
|
||||
base.DeactivatePlugIn(plugIn);
|
||||
if (this._effectiveStrategies.TryGetValue(plugIn.Key, out var effective) && effective == plugIn)
|
||||
{
|
||||
this._effectiveStrategies.Remove(plugIn.Key);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the effective plugin.
|
||||
/// </summary>
|
||||
/// <param name="plugIn">The plugin.</param>
|
||||
protected void SetEffectivePlugin(TPlugIn plugIn)
|
||||
{
|
||||
this._effectiveStrategies.Remove(plugIn.Key);
|
||||
this._effectiveStrategies.Add(plugIn.Key, plugIn);
|
||||
}
|
||||
}
|
||||
80
src/PlugIns/SyntaxTreeExtensions.cs
Normal file
80
src/PlugIns/SyntaxTreeExtensions.cs
Normal file
@@ -0,0 +1,80 @@
|
||||
// <copyright file="SyntaxTreeExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for <see cref="SyntaxTree"/>s.
|
||||
/// </summary>
|
||||
public static class SyntaxTreeExtensions
|
||||
{
|
||||
private static IList<PortableExecutableReference>? _assemblyReferences;
|
||||
|
||||
private static IList<PortableExecutableReference> AssemblyReferences
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_assemblyReferences is { })
|
||||
{
|
||||
return _assemblyReferences;
|
||||
}
|
||||
|
||||
// Force Nito assemblies to be loaded, so they are part of the trusted platform assemblies.
|
||||
_ = Directory.EnumerateFiles(new FileInfo(typeof(SyntaxTreeExtensions).Assembly.Location).DirectoryName!, "Nito.*.dll")
|
||||
.Select(Assembly.LoadFrom)
|
||||
.ToList();
|
||||
|
||||
var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies()
|
||||
.Where(a => !a.IsDynamic)
|
||||
.Select(a => a.Location)
|
||||
.ToImmutableHashSet();
|
||||
var separator = (Environment.OSVersion.Platform == PlatformID.MacOSX ||
|
||||
Environment.OSVersion.Platform == PlatformID.Unix)
|
||||
? ':'
|
||||
: ';';
|
||||
|
||||
_assemblyReferences = (AppContext.GetData("TRUSTED_PLATFORM_ASSEMBLIES") as string)
|
||||
?.Split(separator)
|
||||
.Select(path => MetadataReference.CreateFromFile(path))
|
||||
.Where(metaData => metaData.FilePath is not null && loadedAssemblies.Contains(metaData.FilePath))
|
||||
.ToList() ?? new List<PortableExecutableReference>();
|
||||
return _assemblyReferences;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compiles the <see cref="SyntaxTree"/> and load its assembly into memory.
|
||||
/// </summary>
|
||||
/// <param name="syntaxTree">The syntax tree.</param>
|
||||
/// <param name="assemblyName">Name of the assembly.</param>
|
||||
/// <returns>The compiled assembly.</returns>
|
||||
public static Assembly CompileAndLoad(this SyntaxTree syntaxTree, string assemblyName)
|
||||
{
|
||||
var options = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
|
||||
.WithOverflowChecks(false)
|
||||
.WithOptimizationLevel(OptimizationLevel.Release)
|
||||
.WithUsings("System", "System.Collections.Generic", "System.Threading", "Nito.AsyncEx");
|
||||
var compilation = CSharpCompilation.Create(assemblyName, new[] { syntaxTree }, AssemblyReferences, options);
|
||||
using var stream = new MemoryStream();
|
||||
var result = compilation.Emit(stream);
|
||||
if (!result.Success)
|
||||
{
|
||||
var stringBuilder = new StringBuilder();
|
||||
result.Diagnostics
|
||||
.Where(m => m.Severity == DiagnosticSeverity.Error)
|
||||
.Select(d => $"{d.GetMessage()} @ {d.Location}")
|
||||
.ToList()
|
||||
.ForEach(message => stringBuilder.AppendLine(message));
|
||||
throw new ArgumentException(stringBuilder.ToString());
|
||||
}
|
||||
|
||||
return Assembly.Load(stream.ToArray());
|
||||
}
|
||||
}
|
||||
54
src/PlugIns/TypeExtensions.cs
Normal file
54
src/PlugIns/TypeExtensions.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
// <copyright file="TypeExtensions.cs" company="MUnique">
|
||||
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
||||
// </copyright>
|
||||
|
||||
namespace MUnique.OpenMU.PlugIns;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for <see cref="Type"/>.
|
||||
/// </summary>
|
||||
public static class TypeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the implemented interface of <see cref="ISupportCustomConfiguration{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
/// <returns>The implemented interface of <see cref="ISupportCustomConfiguration{T}"/>.</returns>
|
||||
public static Type? GetCustomConfigurationSupportInterfaceType(this Type plugInType)
|
||||
{
|
||||
return plugInType.GetInterfaces()
|
||||
.Where(i => i.IsGenericType)
|
||||
.FirstOrDefault(i => i.GetGenericTypeDefinition() == typeof(ISupportCustomConfiguration<>));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generic type parameter of <see cref="ISupportCustomConfiguration{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="plugInType">Type of the plug in.</param>
|
||||
/// <returns>The generic type parameter of <see cref="ISupportCustomConfiguration{T}"/>.</returns>
|
||||
public static Type? GetCustomConfigurationType(this Type plugInType)
|
||||
{
|
||||
if (plugInType.GetCustomConfigurationSupportInterfaceType() is { } configSupportInterface)
|
||||
{
|
||||
return configSupportInterface.GenericTypeArguments[0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether this type is a <see cref="Nullable{T}"/>.
|
||||
/// </summary>
|
||||
/// <param name="type">The type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified type is a nullable; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsNullable(this Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the generic type argument of the nullable.
|
||||
/// </summary>
|
||||
/// <param name="type">The nullable type.</param>
|
||||
/// <returns>The generic type argument of the nullable.</returns>
|
||||
public static Type GetTypeOfNullable(this Type type) => type.IsNullable() ? type.GetGenericArguments().First() : type;
|
||||
}
|
||||
Reference in New Issue
Block a user