Interface support for plugin DI (#1235)

* feat: interface support for plugin DI
* attribute to indicate resolvability should be on the service instead of the interface
This commit is contained in:
goat 2023-06-13 20:10:47 +02:00 committed by GitHub
parent 7eb05ddae2
commit 284001ce6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 69 additions and 11 deletions

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using Dalamud.IoC; using Dalamud.IoC;
using Dalamud.IoC.Internal; using Dalamud.IoC.Internal;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.Game.UI;
using Serilog; using Serilog;
@ -14,7 +15,8 @@ namespace Dalamud.Game.ClientState.Aetherytes;
[PluginInterface] [PluginInterface]
[InterfaceVersion("1.0")] [InterfaceVersion("1.0")]
[ServiceManager.BlockingEarlyLoadedService] [ServiceManager.BlockingEarlyLoadedService]
public sealed unsafe partial class AetheryteList : IServiceType [ResolveVia<IAetheryteList>]
public sealed unsafe partial class AetheryteList : IServiceType, IAetheryteList
{ {
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly ClientState clientState = Service<ClientState>.Get(); private readonly ClientState clientState = Service<ClientState>.Get();
@ -27,9 +29,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}"); Log.Verbose($"Teleport address 0x{((nint)this.telepoInstance).ToInt64():X}");
} }
/// <summary> /// <inheritdoc/>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
public int Length public int Length
{ {
get get
@ -46,11 +46,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
} }
} }
/// <summary> /// <inheritdoc/>
/// Gets a Aetheryte Entry at the specified index.
/// </summary>
/// <param name="index">Index.</param>
/// <returns>A <see cref="AetheryteEntry"/> at the specified index.</returns>
public AetheryteEntry? this[int index] public AetheryteEntry? this[int index]
{ {
get get
@ -80,7 +76,7 @@ public sealed unsafe partial class AetheryteList : IServiceType
/// <summary> /// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window. /// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary> /// </summary>
public sealed partial class AetheryteList : IReadOnlyCollection<AetheryteEntry> public sealed partial class AetheryteList
{ {
/// <inheritdoc/> /// <inheritdoc/>
public int Count => this.Length; public int Count => this.Length;

View file

@ -0,0 +1,21 @@
using System;
namespace Dalamud.IoC.Internal;
/// <summary>
/// Indicates that an interface a service can implement can be used to resolve that service.
/// Take care: only one service can implement an interface with this attribute at a time.
/// </summary>
/// <typeparam name="T">The interface that can be used to resolve the service.</typeparam>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class ResolveViaAttribute<T> : ResolveViaAttribute
{
}
/// <summary>
/// Helper class used for matching. Use the generic version.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
internal class ResolveViaAttribute : Attribute
{
}

View file

@ -1,12 +1,12 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Types;
namespace Dalamud.IoC.Internal; namespace Dalamud.IoC.Internal;
@ -18,6 +18,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private static readonly ModuleLog Log = new("SERVICECONTAINER"); private static readonly ModuleLog Log = new("SERVICECONTAINER");
private readonly Dictionary<Type, ObjectInstance> instances = new(); private readonly Dictionary<Type, ObjectInstance> instances = new();
private readonly Dictionary<Type, Type> interfaceToTypeMap = new();
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ServiceContainer"/> class. /// Initializes a new instance of the <see cref="ServiceContainer"/> class.
@ -39,6 +40,20 @@ internal class ServiceContainer : IServiceProvider, IServiceType
} }
this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T)); this.instances[typeof(T)] = new(instance.ContinueWith(x => new WeakReference(x.Result)), typeof(T));
var resolveViaTypes = typeof(T)
.GetCustomAttributes()
.OfType<ResolveViaAttribute>()
.Select(x => x.GetType().GetGenericArguments().First());
foreach (var resolvableType in resolveViaTypes)
{
Log.Verbose("=> {InterfaceName} provides for {TName}", resolvableType.FullName ?? "???", typeof(T).FullName ?? "???");
Debug.Assert(!this.interfaceToTypeMap.ContainsKey(resolvableType), "A service already implements this interface, this is not allowed");
Debug.Assert(typeof(T).IsAssignableTo(resolvableType), "Service does not inherit from indicated ResolveVia type");
this.interfaceToTypeMap[resolvableType] = typeof(T);
}
} }
/// <summary> /// <summary>
@ -233,6 +248,9 @@ internal class ServiceContainer : IServiceProvider, IServiceType
private async Task<object?> GetService(Type serviceType) private async Task<object?> GetService(Type serviceType)
{ {
if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType))
serviceType = implementingType;
if (!this.instances.TryGetValue(serviceType, out var service)) if (!this.instances.TryGetValue(serviceType, out var service))
return null; return null;

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using Dalamud.Game.ClientState.Aetherytes;
namespace Dalamud.Plugin.Services;
/// <summary>
/// This collection represents the list of available Aetherytes in the Teleport window.
/// </summary>
public interface IAetheryteList : IReadOnlyCollection<AetheryteEntry>
{
/// <summary>
/// Gets the amount of Aetherytes the local player has unlocked.
/// </summary>
public int Length { get; }
/// <summary>
/// Gets a Aetheryte Entry at the specified index.
/// </summary>
/// <param name="index">Index.</param>
/// <returns>A <see cref="AetheryteEntry"/> at the specified index.</returns>
public AetheryteEntry? this[int index] { get; }
}