Implement ioc container

This commit is contained in:
Raymond 2021-08-20 12:46:16 -04:00
parent ff1d7f2829
commit 2fe8ccb1da
14 changed files with 247 additions and 248 deletions

View file

@ -17,16 +17,11 @@ namespace Dalamud.CorePlugin
// private Localization localizationManager;
/// <inheritdoc/>
public string Name => "Dalamud.CorePlugin";
/// <summary>
/// Gets the plugin interface.
/// Initializes a new instance of the <see cref="PluginImpl"/> class.
/// </summary>
internal DalamudPluginInterface Interface { get; private set; }
/// <inheritdoc/>
public void Initialize(DalamudPluginInterface pluginInterface)
/// <param name="pluginInterface">Dalamud plugin interface.</param>
public PluginImpl(DalamudPluginInterface pluginInterface)
{
try
{
@ -38,8 +33,7 @@ namespace Dalamud.CorePlugin
this.Interface.UiBuilder.Draw += this.OnDraw;
this.Interface.UiBuilder.OpenConfigUi += this.OnOpenConfigUi;
var commandManager = Service<CommandManager>.Get();
commandManager.AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
Service<CommandManager>.Get().AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." });
}
catch (Exception ex)
{
@ -47,10 +41,18 @@ namespace Dalamud.CorePlugin
}
}
/// <inheritdoc/>
public string Name => "Dalamud.CorePlugin";
/// <summary>
/// Gets the plugin interface.
/// </summary>
internal DalamudPluginInterface Interface { get; private set; }
/// <inheritdoc/>
public void Dispose()
{
this.Interface.CommandManager.RemoveHandler("/di");
Service<CommandManager>.Get().RemoveHandler("/di");
this.Interface.UiBuilder.Draw -= this.OnDraw;

View file

@ -91,10 +91,11 @@ namespace Dalamud
{
try
{
Service<ServiceContainer>.Set();
// Initialize the process information.
Service<SigScanner>.Set(new SigScanner(true));
Service<HookManager>.Set();
Service<ServiceContainer>.Set();
// Initialize FFXIVClientStructs function resolver
FFXIVClientStructs.Resolver.Initialize();

View file

@ -1,10 +1,15 @@
using System;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
namespace Dalamud.Game.ClientState.Conditions
{
/// <summary>
/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class Condition
{
/// <summary>

View file

@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Reflection;
using Dalamud.Game.ClientState.JobGauge.Types;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Serilog;
namespace Dalamud.Game.ClientState.JobGauge
@ -10,6 +12,8 @@ namespace Dalamud.Game.ClientState.JobGauge
/// <summary>
/// This class converts in-memory Job gauge data to structs.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
public class JobGauges
{
private Dictionary<Type, JobGaugeBase> cache = new();

View file

@ -46,11 +46,10 @@ namespace Dalamud.Game.Gui
/// </summary>
internal GameGui()
{
this.address = new GameGuiAddressResolver(this.address.BaseAddress);
this.address = new GameGuiAddressResolver();
this.address.Setup();
Log.Verbose("===== G A M E G U I =====");
Log.Verbose($"GameGuiManager address 0x{this.address.BaseAddress.ToInt64():X}");
Log.Verbose($"SetGlobalBgm address 0x{this.address.SetGlobalBgm.ToInt64():X}");
Log.Verbose($"HandleItemHover address 0x{this.address.HandleItemHover.ToInt64():X}");

View file

@ -11,10 +11,9 @@ namespace Dalamud.Game.Gui
/// <summary>
/// Initializes a new instance of the <see cref="GameGuiAddressResolver"/> class.
/// </summary>
/// <param name="baseAddress">The base address of the native GuiManager class.</param>
public GameGuiAddressResolver(IntPtr baseAddress)
public GameGuiAddressResolver()
{
this.BaseAddress = baseAddress;
this.BaseAddress = Service<Framework>.Get().Address.BaseAddress;
}
/// <summary>

View file

@ -1,7 +1,8 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin;
using ImGuiNET;
using Microsoft.CodeAnalysis.CSharp.Scripting;
@ -79,13 +80,18 @@ namespace Dalamud.Interface.Internal.Scratchpad
{
var script = CSharpScript.Create(code, options);
var pi = new DalamudPluginInterface("Scratch-" + doc.Id, PluginLoadReason.Unknown);
var plugin = script.ContinueWith<IDalamudPlugin>("return new ScratchPlugin() as IDalamudPlugin;")
var pluginType = script.ContinueWith<Type>("return typeof(ScratchPlugin);")
.RunAsync().GetAwaiter().GetResult().ReturnValue;
plugin.Initialize(pi);
var pi = new DalamudPluginInterface($"Scratch-{doc.Id}", PluginLoadReason.Unknown);
this.loadedScratches[doc.Id] = plugin;
var ioc = Service<ServiceContainer>.Get();
var plugin = ioc.Create(pluginType, pi);
if (plugin == null)
throw new Exception("Could not initialize scratch plugin");
this.loadedScratches[doc.Id] = (IDalamudPlugin)plugin;
return ScratchLoadStatus.Success;
}
catch (CompilationErrorException ex)

View file

@ -21,7 +21,7 @@ public class ScratchPlugin : IDalamudPlugin {
{SETUPBODY}
public void Initialize(DalamudPluginInterface pluginInterface)
public ScratchPlugin(DalamudPluginInterface pluginInterface)
{
this.pi = pluginInterface;

View file

@ -610,53 +610,6 @@ namespace Dalamud.Interface.Internal.Windows
private void DrawPluginIPC()
{
#pragma warning disable CS0618 // Type or member is obsolete
var i1 = new DalamudPluginInterface("DalamudTestSub", PluginLoadReason.Unknown);
var i2 = new DalamudPluginInterface("DalamudTestPub", PluginLoadReason.Unknown);
if (ImGui.Button("Add test sub"))
{
i1.Subscribe("DalamudTestPub", o =>
{
dynamic msg = o;
Log.Debug(msg.Expand);
});
}
if (ImGui.Button("Add test sub any"))
{
i1.SubscribeAny((o, a) =>
{
dynamic msg = a;
Log.Debug($"From {o}: {msg.Expand}");
});
}
if (ImGui.Button("Remove test sub"))
i1.Unsubscribe("DalamudTestPub");
if (ImGui.Button("Remove test sub any"))
i1.UnsubscribeAny();
if (ImGui.Button("Send test message"))
{
dynamic testMsg = new ExpandoObject();
testMsg.Expand = "dong";
i2.SendMessage(testMsg);
}
// This doesn't actually work, so don't mind it - impl relies on plugins being registered in PluginManager
if (ImGui.Button("Send test message any"))
{
dynamic testMsg = new ExpandoObject();
testMsg.Expand = "dong";
i2.SendMessage("DalamudTestSub", testMsg);
}
var pluginManager = Service<PluginManager>.Get();
foreach (var ipc in pluginManager.IpcSubscriptions)
ImGui.Text($"Source:{ipc.SourcePluginName} Sub:{ipc.SubPluginName}");
#pragma warning restore CS0618 // Type or member is obsolete
}
private void DrawCondition()

View file

@ -0,0 +1,31 @@
using System;
using System.Reflection;
namespace Dalamud.IoC.Internal
{
/// <summary>
/// An object instance registered in the <see cref="ServiceContainer"/>.
/// </summary>
internal class ObjectInstance
{
/// <summary>
/// Initializes a new instance of the <see cref="ObjectInstance"/> class.
/// </summary>
/// <param name="instance">The underlying instance.</param>
public ObjectInstance(object instance)
{
this.Instance = new WeakReference(instance);
this.Version = instance.GetType().GetCustomAttribute<InterfaceVersionAttribute>();
}
/// <summary>
/// Gets the current version of the instance, if it exists.
/// </summary>
public InterfaceVersionAttribute? Version { get; }
/// <summary>
/// Gets a reference to the underlying instance.
/// </summary>
public WeakReference Instance { get; }
}
}

View file

@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Serilog;
namespace Dalamud.IoC.Internal
{
/// <summary>
/// A simple singleton-only IOC container that provides (optional) version-based dependency resolution.
/// </summary>
internal class ServiceContainer : IServiceProvider
{
private readonly Dictionary<Type, ObjectInstance> instances = new();
/// <summary>
/// Register a singleton object of any type into the current IOC container.
/// </summary>
/// <param name="instance">The existing instance to register in the container.</param>
/// <typeparam name="T">The interface to register.</typeparam>
public void RegisterSingleton<T>(T instance)
{
if (instance == null)
{
throw new ArgumentNullException(nameof(instance));
}
this.instances[typeof(T)] = new(instance);
}
/// <summary>
/// Create an object.
/// </summary>
/// <param name="objectType">The type of object to create.</param>
/// <param name="scopedObjects">Scoped objects to be included in the constructor.</param>
/// <returns>The created object.</returns>
public object? Create(Type objectType, params object[] scopedObjects)
{
var ctor = this.FindApplicableCtor(objectType, scopedObjects);
if (ctor == null)
{
Log.Error("Failed to create {TypeName}, unable to find any services to satisfy the dependencies in the ctor", objectType.FullName);
return null;
}
// validate dependency versions (if they exist)
var parameters = ctor.GetParameters().Select(p =>
{
var parameterType = p.ParameterType;
var requiredVersion = p.GetCustomAttribute(typeof(RequiredVersionAttribute)) as RequiredVersionAttribute;
return (parameterType, requiredVersion);
});
var versionCheck = parameters.Any(p =>
{
// if there's no required version, ignore it
if (p.requiredVersion == null)
return true;
// if there's no requested version, ignore it
var declVersion = p.parameterType.GetCustomAttribute<InterfaceVersionAttribute>();
if (declVersion == null)
return true;
if (declVersion.Version == p.requiredVersion.Version)
return true;
Log.Error(
"Requested version {ReqVersion} does not match the implemented version {ImplVersion} for param type {ParamType}",
p.requiredVersion.Version,
declVersion.Version,
p.parameterType.FullName);
return false;
});
if (!versionCheck)
{
Log.Error("Failed to create {TypeName}, a RequestedVersion could not be satisfied", objectType.FullName);
return null;
}
var resolvedParams = parameters
.Select(p =>
{
var service = this.GetService(p.parameterType, scopedObjects);
if (service == null)
{
Log.Error("Requested service type {TypeName} could not be satisfied", p.parameterType.FullName);
}
return service;
})
.ToArray();
var hasNull = resolvedParams.Any(p => p == null);
if (hasNull)
{
Log.Error("Failed to create {TypeName}, a requested service type could not be satisfied", objectType.FullName);
return null;
}
return Activator.CreateInstance(objectType, resolvedParams);
}
/// <inheritdoc/>
object? IServiceProvider.GetService(Type serviceType) => this.GetService(serviceType);
private object? GetService(Type serviceType, object[] scopedObjects)
{
var singletonService = this.GetService(serviceType);
if (singletonService != null)
{
return singletonService;
}
// resolve dependency from scoped objects
var scoped = scopedObjects.FirstOrDefault(o => o.GetType() == serviceType);
if (scoped == default)
{
return null;
}
return scoped;
}
private object? GetService(Type serviceType)
{
var hasInstance = this.instances.TryGetValue(serviceType, out var service);
if (hasInstance && service.Instance.IsAlive)
{
return service.Instance.Target;
}
return null;
}
private ConstructorInfo? FindApplicableCtor(Type type, object[] scopedObjects)
{
// get a list of all the available types: scoped and singleton
var types = scopedObjects
.Select(o => o.GetType())
.Union(this.instances.Keys);
var ctors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
foreach (var ctor in ctors)
{
var parameters = ctor.GetParameters();
var success = parameters.All(p => types.Contains(p.ParameterType));
if (success)
return ctor;
}
return null;
}
}
}

View file

@ -39,22 +39,17 @@ namespace Dalamud.Plugin
internal DalamudPluginInterface(string pluginName, PluginLoadReason reason)
{
var configuration = Service<DalamudConfiguration>.Get();
var dataManager = Service<DataManager>.Get();
var localization = Service<Localization>.Get();
this.CommandManager = Service<CommandManager>.Get();
this.Framework = Service<Framework>.Get();
this.ClientState = Service<ClientState>.Get();
this.UiBuilder = new UiBuilder(pluginName);
this.TargetModuleScanner = Service<SigScanner>.Get();
this.Data = Service<DataManager>.Get();
this.SeStringManager = Service<SeStringManager>.Get();
this.pluginName = pluginName;
this.configs = Service<PluginManager>.Get().PluginConfigs;
this.Reason = reason;
this.GeneralChatType = configuration.GeneralChatType;
this.Sanitizer = new Sanitizer(this.Data.Language);
this.Sanitizer = new Sanitizer(dataManager.Language);
if (configuration.LanguageOverride != null)
{
this.UiLanguage = configuration.LanguageOverride;
@ -103,41 +98,11 @@ namespace Dalamud.Plugin
/// </summary>
public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName);
/// <summary>
/// Gets the CommandManager object that allows you to add and remove custom chat commands.
/// </summary>
public CommandManager CommandManager { get; private set; }
/// <summary>
/// Gets the ClientState object that allows you to access current client memory information like actors, territories, etc.
/// </summary>
public ClientState ClientState { get; private set; }
/// <summary>
/// Gets the Framework object that allows you to interact with the client.
/// </summary>
public Framework Framework { get; private set; }
/// <summary>
/// Gets the <see cref="UiBuilder"/> instance which allows you to draw UI into the game via ImGui draw calls.
/// </summary>
public UiBuilder UiBuilder { get; private set; }
/// <summary>
/// Gets the <see cref="SigScanner">SigScanner</see> instance targeting the main module of the FFXIV process.
/// </summary>
public SigScanner TargetModuleScanner { get; private set; }
/// <summary>
/// Gets the <see cref="DataManager">DataManager</see> instance which allows you to access game data needed by the main dalamud features.
/// </summary>
public DataManager Data { get; private set; }
/// <summary>
/// Gets the <see cref="SeStringManager">SeStringManager</see> instance which allows creating and parsing SeString payloads.
/// </summary>
public SeStringManager SeStringManager { get; private set; }
/// <summary>
/// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds.
/// </summary>
@ -162,11 +127,6 @@ namespace Dalamud.Plugin
/// </summary>
public XivChatType GeneralChatType { get; private set; }
/// <summary>
/// Gets the action that should be executed when any plugin sends a message.
/// </summary>
internal Action<string, ExpandoObject> AnyPluginIpcAction { get; private set; }
#region Configuration
/// <summary>
@ -253,107 +213,6 @@ namespace Dalamud.Plugin
}
#endregion
#region IPC
/// <summary>
/// Subscribe to an IPC message by any plugin.
/// </summary>
/// <param name="action">The action to take when a message was received.</param>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public void SubscribeAny(Action<string, ExpandoObject> action)
{
if (this.AnyPluginIpcAction != null)
throw new InvalidOperationException("Can't subscribe multiple times.");
this.AnyPluginIpcAction = action;
}
/// <summary>
/// Subscribe to an IPC message by a plugin.
/// </summary>
/// <param name="pluginName">The InternalName of the plugin to subscribe to.</param>
/// <param name="action">The action to take when a message was received.</param>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public void Subscribe(string pluginName, Action<ExpandoObject> action)
{
var pluginManager = Service<PluginManager>.Get();
if (pluginManager.IpcSubscriptions.Any(x => x.SourcePluginName == this.pluginName && x.SubPluginName == pluginName))
throw new InvalidOperationException("Can't add multiple subscriptions for the same plugin.");
pluginManager.IpcSubscriptions.Add(new(this.pluginName, pluginName, action));
}
/// <summary>
/// Unsubscribe from messages from any plugin.
/// </summary>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public void UnsubscribeAny()
{
if (this.AnyPluginIpcAction == null)
throw new InvalidOperationException("Wasn't subscribed to this plugin.");
this.AnyPluginIpcAction = null;
}
/// <summary>
/// Unsubscribe from messages from a plugin.
/// </summary>
/// <param name="pluginName">The InternalName of the plugin to unsubscribe from.</param>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public void Unsubscribe(string pluginName)
{
var pluginManager = Service<PluginManager>.Get();
var sub = pluginManager.IpcSubscriptions.FirstOrDefault(x => x.SourcePluginName == this.pluginName && x.SubPluginName == pluginName);
if (sub.SubAction == null)
throw new InvalidOperationException("Wasn't subscribed to this plugin.");
pluginManager.IpcSubscriptions.Remove(sub);
}
/// <summary>
/// Send a message to all subscribed plugins.
/// </summary>
/// <param name="message">The message to send.</param>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public void SendMessage(ExpandoObject message)
{
var pluginManager = Service<PluginManager>.Get();
var subs = pluginManager.IpcSubscriptions.Where(x => x.SubPluginName == this.pluginName);
foreach (var sub in subs.Select(x => x.SubAction))
{
sub.Invoke(message);
}
}
/// <summary>
/// Send a message to a specific plugin.
/// </summary>
/// <param name="pluginName">The InternalName of the plugin to send the message to.</param>
/// <param name="message">The message to send.</param>
/// <returns>True if the corresponding plugin was present and received the message.</returns>
[Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")]
public bool SendMessage(string pluginName, ExpandoObject message)
{
var pluginManager = Service<PluginManager>.Get();
var plugin = pluginManager.InstalledPlugins.FirstOrDefault(x => x.Manifest.InternalName == pluginName);
if (plugin == default)
return false;
if (plugin.DalamudInterface?.AnyPluginIpcAction == null)
return false;
plugin.DalamudInterface.AnyPluginIpcAction.Invoke(this.pluginName, message);
return true;
}
#endregion
/// <summary>
/// Unregister your plugin and dispose all references. You have to call this when your IDalamudPlugin is disposed.
/// </summary>

View file

@ -11,11 +11,5 @@ namespace Dalamud.Plugin
/// Gets the name of the plugin.
/// </summary>
string Name { get; }
/// <summary>
/// Initializes a Dalamud plugin.
/// </summary>
/// <param name="pluginInterface">The <see cref="DalamudPluginInterface"/> needed to access various Dalamud objects.</param>
void Initialize(DalamudPluginInterface pluginInterface);
}
}

View file

@ -5,6 +5,7 @@ using System.Reflection;
using Dalamud.Configuration.Internal;
using Dalamud.Game;
using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal;
using Dalamud.Plugin.Internal.Exceptions;
using Dalamud.Plugin.Internal.Types;
@ -26,8 +27,8 @@ namespace Dalamud.Plugin.Internal
private PluginLoader loader;
private Assembly pluginAssembly;
private Type pluginType;
private IDalamudPlugin instance;
private Type? pluginType;
private IDalamudPlugin? instance;
/// <summary>
/// Initializes a new instance of the <see cref="LocalPlugin"/> class.
@ -264,8 +265,16 @@ namespace Dalamud.Plugin.Internal
// Update the location for the Location and CodeBase patches
PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new(this.DllFile);
// Instantiate and initialize
this.instance = Activator.CreateInstance(this.pluginType) as IDalamudPlugin;
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, reason);
var ioc = Service<ServiceContainer>.Get();
this.instance = ioc.Create(this.pluginType, this.DalamudInterface) as IDalamudPlugin;
if (this.instance == null)
{
this.State = PluginState.LoadError;
Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor");
return;
}
// In-case the manifest name was a placeholder. Can occur when no manifest was included.
if (this.instance.Name != this.Manifest.Name)
@ -274,30 +283,6 @@ namespace Dalamud.Plugin.Internal
this.Manifest.Save(this.manifestFile);
}
this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name, reason);
if (this.IsDev)
{
// Inherit LPL's AssemblyLocation functionality
try
{
var bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
this.instance.GetType()
?.GetProperty("AssemblyLocation", bindingFlags)
?.SetValue(this.instance, this.DllFile.FullName);
this.instance.GetType()
?.GetMethod("SetLocation", bindingFlags)
?.Invoke(this.instance, new object[] { this.DllFile.FullName });
}
catch
{
// Ignored
}
}
this.instance.Initialize(this.DalamudInterface);
this.State = PluginState.Loaded;
Log.Information($"Finished loading {this.DllFile.Name}");
}