baseline: OpenMU upstream b5a0961 (fresh source)

This commit is contained in:
Acentech Dev
2026-07-14 19:00:35 +03:00
parent 450402e47d
commit 36fc125d5c
11968 changed files with 748705 additions and 0 deletions

View 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();
}
}

View 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;
}
}
}

View 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; }
}

View 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)));
}

View 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;
}

View 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);
}

View 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; }
}

View 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; }
}

View 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();
}

View 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();
}
}

View 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;
}
}

View 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>

View 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;

View 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>();
}
}
}

View 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;
}
}

View 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; }
}

View 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;
}
}

View 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 });
}
}
}

View 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; }
}

View 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}";
}
}
}

View 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; }
}

View 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;
}
}

View 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
View 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.

View 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);
}
}

View 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());
}
}

View 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;
}