diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index d6e472277..83dc32c97 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -17,16 +17,11 @@ namespace Dalamud.CorePlugin // private Localization localizationManager; - /// - public string Name => "Dalamud.CorePlugin"; - /// - /// Gets the plugin interface. + /// Initializes a new instance of the class. /// - internal DalamudPluginInterface Interface { get; private set; } - - /// - public void Initialize(DalamudPluginInterface pluginInterface) + /// Dalamud plugin interface. + 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.Get(); - commandManager.AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); + Service.Get().AddHandler("/di", new(this.OnCommand) { HelpMessage = $"Access the {this.Name} plugin." }); } catch (Exception ex) { @@ -47,10 +41,18 @@ namespace Dalamud.CorePlugin } } + /// + public string Name => "Dalamud.CorePlugin"; + + /// + /// Gets the plugin interface. + /// + internal DalamudPluginInterface Interface { get; private set; } + /// public void Dispose() { - this.Interface.CommandManager.RemoveHandler("/di"); + Service.Get().RemoveHandler("/di"); this.Interface.UiBuilder.Draw -= this.OnDraw; diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 01e1a151d..838d5c7c8 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -91,10 +91,11 @@ namespace Dalamud { try { + Service.Set(); + // Initialize the process information. Service.Set(new SigScanner(true)); Service.Set(); - Service.Set(); // Initialize FFXIVClientStructs function resolver FFXIVClientStructs.Resolver.Initialize(); diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index 5a4322532..4d679a180 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -1,10 +1,15 @@ using System; +using Dalamud.IoC; +using Dalamud.IoC.Internal; + namespace Dalamud.Game.ClientState.Conditions { /// /// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. /// + [PluginInterface] + [InterfaceVersion("1.0")] public class Condition { /// diff --git a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs index f3c836e10..958f78b1b 100644 --- a/Dalamud/Game/ClientState/JobGauge/JobGauges.cs +++ b/Dalamud/Game/ClientState/JobGauge/JobGauges.cs @@ -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 /// /// This class converts in-memory Job gauge data to structs. /// + [PluginInterface] + [InterfaceVersion("1.0")] public class JobGauges { private Dictionary cache = new(); diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index 6c65abf4b..1eaa9c60b 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -46,11 +46,10 @@ namespace Dalamud.Game.Gui /// 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}"); diff --git a/Dalamud/Game/Gui/GameGuiAddressResolver.cs b/Dalamud/Game/Gui/GameGuiAddressResolver.cs index 52e98df6c..122a9eea2 100644 --- a/Dalamud/Game/Gui/GameGuiAddressResolver.cs +++ b/Dalamud/Game/Gui/GameGuiAddressResolver.cs @@ -11,10 +11,9 @@ namespace Dalamud.Game.Gui /// /// Initializes a new instance of the class. /// - /// The base address of the native GuiManager class. - public GameGuiAddressResolver(IntPtr baseAddress) + public GameGuiAddressResolver() { - this.BaseAddress = baseAddress; + this.BaseAddress = Service.Get().Address.BaseAddress; } /// diff --git a/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs index eba0b3d2a..cb3133951 100644 --- a/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchExecutionManager.cs @@ -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("return new ScratchPlugin() as IDalamudPlugin;") + var pluginType = script.ContinueWith("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.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) diff --git a/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs index 0a2e4e8fb..9ee145394 100644 --- a/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs +++ b/Dalamud/Interface/Internal/Scratchpad/ScratchMacroProcessor.cs @@ -21,7 +21,7 @@ public class ScratchPlugin : IDalamudPlugin { {SETUPBODY} - public void Initialize(DalamudPluginInterface pluginInterface) + public ScratchPlugin(DalamudPluginInterface pluginInterface) { this.pi = pluginInterface; diff --git a/Dalamud/Interface/Internal/Windows/DataWindow.cs b/Dalamud/Interface/Internal/Windows/DataWindow.cs index d49ee8524..8fb88ef8a 100644 --- a/Dalamud/Interface/Internal/Windows/DataWindow.cs +++ b/Dalamud/Interface/Internal/Windows/DataWindow.cs @@ -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.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() diff --git a/Dalamud/IoC/Internal/ObjectInstance.cs b/Dalamud/IoC/Internal/ObjectInstance.cs new file mode 100644 index 000000000..7475fcfaf --- /dev/null +++ b/Dalamud/IoC/Internal/ObjectInstance.cs @@ -0,0 +1,31 @@ +using System; +using System.Reflection; + +namespace Dalamud.IoC.Internal +{ + /// + /// An object instance registered in the . + /// + internal class ObjectInstance + { + /// + /// Initializes a new instance of the class. + /// + /// The underlying instance. + public ObjectInstance(object instance) + { + this.Instance = new WeakReference(instance); + this.Version = instance.GetType().GetCustomAttribute(); + } + + /// + /// Gets the current version of the instance, if it exists. + /// + public InterfaceVersionAttribute? Version { get; } + + /// + /// Gets a reference to the underlying instance. + /// + public WeakReference Instance { get; } + } +} diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs new file mode 100644 index 000000000..e1604c188 --- /dev/null +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -0,0 +1,161 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Serilog; + +namespace Dalamud.IoC.Internal +{ + /// + /// A simple singleton-only IOC container that provides (optional) version-based dependency resolution. + /// + internal class ServiceContainer : IServiceProvider + { + private readonly Dictionary instances = new(); + + /// + /// Register a singleton object of any type into the current IOC container. + /// + /// The existing instance to register in the container. + /// The interface to register. + public void RegisterSingleton(T instance) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + this.instances[typeof(T)] = new(instance); + } + + /// + /// Create an object. + /// + /// The type of object to create. + /// Scoped objects to be included in the constructor. + /// The created object. + 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(); + 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); + } + + /// + 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; + } + } +} diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e5494ea91..942e01b9c 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -39,22 +39,17 @@ namespace Dalamud.Plugin internal DalamudPluginInterface(string pluginName, PluginLoadReason reason) { var configuration = Service.Get(); + var dataManager = Service.Get(); var localization = Service.Get(); - this.CommandManager = Service.Get(); - this.Framework = Service.Get(); - this.ClientState = Service.Get(); this.UiBuilder = new UiBuilder(pluginName); - this.TargetModuleScanner = Service.Get(); - this.Data = Service.Get(); - this.SeStringManager = Service.Get(); this.pluginName = pluginName; this.configs = Service.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 /// public FileInfo ConfigFile => this.configs.GetConfigFile(this.pluginName); - /// - /// Gets the CommandManager object that allows you to add and remove custom chat commands. - /// - public CommandManager CommandManager { get; private set; } - - /// - /// Gets the ClientState object that allows you to access current client memory information like actors, territories, etc. - /// - public ClientState ClientState { get; private set; } - - /// - /// Gets the Framework object that allows you to interact with the client. - /// - public Framework Framework { get; private set; } - /// /// Gets the instance which allows you to draw UI into the game via ImGui draw calls. /// public UiBuilder UiBuilder { get; private set; } - /// - /// Gets the SigScanner instance targeting the main module of the FFXIV process. - /// - public SigScanner TargetModuleScanner { get; private set; } - - /// - /// Gets the DataManager instance which allows you to access game data needed by the main dalamud features. - /// - public DataManager Data { get; private set; } - - /// - /// Gets the SeStringManager instance which allows creating and parsing SeString payloads. - /// - public SeStringManager SeStringManager { get; private set; } - /// /// Gets a value indicating whether Dalamud is running in Debug mode or the /xldev menu is open. This can occur on release builds. /// @@ -162,11 +127,6 @@ namespace Dalamud.Plugin /// public XivChatType GeneralChatType { get; private set; } - /// - /// Gets the action that should be executed when any plugin sends a message. - /// - internal Action AnyPluginIpcAction { get; private set; } - #region Configuration /// @@ -253,107 +213,6 @@ namespace Dalamud.Plugin } #endregion - #region IPC - - /// - /// Subscribe to an IPC message by any plugin. - /// - /// The action to take when a message was received. - [Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")] - public void SubscribeAny(Action action) - { - if (this.AnyPluginIpcAction != null) - throw new InvalidOperationException("Can't subscribe multiple times."); - - this.AnyPluginIpcAction = action; - } - - /// - /// Subscribe to an IPC message by a plugin. - /// - /// The InternalName of the plugin to subscribe to. - /// The action to take when a message was received. - [Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")] - public void Subscribe(string pluginName, Action action) - { - var pluginManager = Service.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)); - } - - /// - /// Unsubscribe from messages from any plugin. - /// - [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; - } - - /// - /// Unsubscribe from messages from a plugin. - /// - /// The InternalName of the plugin to unsubscribe from. - [Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")] - public void Unsubscribe(string pluginName) - { - var pluginManager = Service.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); - } - - /// - /// Send a message to all subscribed plugins. - /// - /// The message to send. - [Obsolete("The current IPC mechanism is obsolete and scheduled to be replaced after API level 3.")] - public void SendMessage(ExpandoObject message) - { - var pluginManager = Service.Get(); - - var subs = pluginManager.IpcSubscriptions.Where(x => x.SubPluginName == this.pluginName); - foreach (var sub in subs.Select(x => x.SubAction)) - { - sub.Invoke(message); - } - } - - /// - /// Send a message to a specific plugin. - /// - /// The InternalName of the plugin to send the message to. - /// The message to send. - /// True if the corresponding plugin was present and received the message. - [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.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 - /// /// Unregister your plugin and dispose all references. You have to call this when your IDalamudPlugin is disposed. /// diff --git a/Dalamud/Plugin/IDalamudPlugin.cs b/Dalamud/Plugin/IDalamudPlugin.cs index aff863eac..51d67328d 100644 --- a/Dalamud/Plugin/IDalamudPlugin.cs +++ b/Dalamud/Plugin/IDalamudPlugin.cs @@ -11,11 +11,5 @@ namespace Dalamud.Plugin /// Gets the name of the plugin. /// string Name { get; } - - /// - /// Initializes a Dalamud plugin. - /// - /// The needed to access various Dalamud objects. - void Initialize(DalamudPluginInterface pluginInterface); } } diff --git a/Dalamud/Plugin/Internal/LocalPlugin.cs b/Dalamud/Plugin/Internal/LocalPlugin.cs index 80983ab2c..7c3c9ef25 100644 --- a/Dalamud/Plugin/Internal/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/LocalPlugin.cs @@ -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,15 +27,15 @@ namespace Dalamud.Plugin.Internal private PluginLoader loader; private Assembly pluginAssembly; - private Type pluginType; - private IDalamudPlugin instance; + private Type? pluginType; + private IDalamudPlugin? instance; /// /// Initializes a new instance of the class. /// /// Path to the DLL file. /// The plugin manifest. - public LocalPlugin(FileInfo dllFile, LocalPluginManifest?manifest) + public LocalPlugin(FileInfo dllFile, LocalPluginManifest? manifest) { this.DllFile = dllFile; this.State = PluginState.Unloaded; @@ -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.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}"); }