From b66be84b939147505a4221a39da4c4b50ec18358 Mon Sep 17 00:00:00 2001 From: srkizer Date: Wed, 29 Nov 2023 06:20:16 +0900 Subject: [PATCH] Better Service dependency handling (#1535) --- .../Internal/DalamudConfiguration.cs | 2 +- Dalamud/Dalamud.cs | 2 +- .../Game/Addon/Events/AddonEventManager.cs | 2 +- .../Game/Addon/Lifecycle/AddonLifecycle.cs | 2 +- Dalamud/Game/Config/GameConfig.cs | 2 +- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../UniversalisMarketBoardUploader.cs | 9 +- .../Game/Network/Internal/NetworkHandlers.cs | 10 +- Dalamud/Game/TargetSigScanner.cs | 2 +- Dalamud/Interface/DragDrop/DragDropManager.cs | 2 +- .../Interface/GameFonts/GameFontManager.cs | 2 +- .../Interface/Internal/DalamudInterface.cs | 106 +++++++------ .../Interface/Internal/InterfaceManager.cs | 2 +- .../Internal/Windows/ChangelogWindow.cs | 14 +- .../Internal/Windows/ConsoleWindow.cs | 5 +- .../PluginInstaller/PluginInstallerWindow.cs | 7 +- .../Internal/Windows/TitleScreenMenuWindow.cs | 56 ++++--- Dalamud/IoC/Internal/ServiceContainer.cs | 7 +- Dalamud/Plugin/Internal/PluginManager.cs | 28 ++-- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 4 - .../Plugin/Internal/Types/PluginRepository.cs | 65 ++++---- Dalamud/Plugin/Ipc/Internal/DataShare.cs | 2 +- Dalamud/ServiceManager.cs | 132 +++++++++++++--- Dalamud/Service{T}.cs | 145 ++++++++++++++---- Dalamud/Storage/ReliableFileStorage.cs | 2 +- 25 files changed, 415 insertions(+), 197 deletions(-) diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 35d5261da..76c8f3603 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -21,7 +21,7 @@ namespace Dalamud.Configuration.Internal; /// Class containing Dalamud settings. /// [Serializable] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index f50a39aa3..9896b87a6 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -30,7 +30,7 @@ namespace Dalamud; /// /// The main Dalamud class containing all subsystems. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal sealed class Dalamud : IServiceType { #region Internals diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index a91f5437c..d8f3427ef 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events; /// Service provider for addon event management. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonEventManager : IDisposable, IServiceType { /// diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index c7184ca11..08a2d59ef 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// This class provides events for in-game addon lifecycles. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class AddonLifecycle : IDisposable, IServiceType { private static readonly ModuleLog Log = new("AddonLifecycle"); diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index ae3205abc..b82d64f24 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Config; /// This class represents the game's configuration. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable { private readonly GameConfigAddressResolver address = new(); diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 6dda95a66..66356033b 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.DutyState; /// This class represents the state of the currently occupied duty. /// [InterfaceVersion("1.0")] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class DutyState : IDisposable, IServiceType, IDutyState { private readonly DutyStateAddressResolver address; diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index b3175cad3..34a255e19 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Net.Http; using System.Text; @@ -22,14 +21,14 @@ internal class UniversalisMarketBoardUploader : IMarketBoardUploader private const string ApiKey = "GGD6RdSfGyRiHM5WDnAo0Nj9Nv7aC5NDhMj3BebT"; - private readonly HttpClient httpClient = Service.Get().SharedHttpClient; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// - public UniversalisMarketBoardUploader() - { - } + /// An instance of . + public UniversalisMarketBoardUploader(HappyHttpClient happyHttpClient) => + this.httpClient = happyHttpClient.SharedHttpClient; /// public async Task Upload(MarketBoardItemRequest request) diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 01e92a373..76d3b5659 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -13,6 +13,7 @@ using Dalamud.Game.Network.Internal.MarketBoardUploaders; using Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis; using Dalamud.Game.Network.Structures; using Dalamud.Hooking; +using Dalamud.Networking.Http; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.UI.Info; using Lumina.Excel.GeneratedSheets; @@ -23,7 +24,7 @@ namespace Dalamud.Game.Network.Internal; /// /// This class handles network notifications and uploading market board data. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal unsafe class NetworkHandlers : IDisposable, IServiceType { private readonly IMarketBoardUploader uploader; @@ -55,9 +56,12 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType private bool disposing; [ServiceManager.ServiceConstructor] - private NetworkHandlers(GameNetwork gameNetwork, TargetSigScanner sigScanner) + private NetworkHandlers( + GameNetwork gameNetwork, + TargetSigScanner sigScanner, + HappyHttpClient happyHttpClient) { - this.uploader = new UniversalisMarketBoardUploader(); + this.uploader = new UniversalisMarketBoardUploader(happyHttpClient); this.addressResolver = new NetworkHandlersAddressResolver(); this.addressResolver.Setup(sigScanner); diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 9242c5e83..35c82562e 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -11,7 +11,7 @@ namespace Dalamud.Game; /// [PluginInterface] [InterfaceVersion("1.0")] -[ServiceManager.Service] +[ServiceManager.ProvidedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index e8641035f..151ef28a0 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Interface.DragDrop; /// and can be used to create ImGui drag and drop sources and targets for those external events. /// [PluginInterface] -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 71661682d..b3454e085 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -22,7 +22,7 @@ namespace Dalamud.Interface.GameFonts; /// /// Loads game font for use in ImGui. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class GameFontManager : IServiceType { private static readonly string?[] FontNames = diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 816352d80..18ab538c4 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -1,7 +1,5 @@ -using System; using System.Diagnostics; using System.Globalization; -using System.IO; using System.Linq; using System.Numerics; using System.Reflection; @@ -9,6 +7,7 @@ using System.Runtime.InteropServices; using CheapLoc; using Dalamud.Configuration.Internal; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Game.Internal; @@ -25,14 +24,14 @@ using Dalamud.Interface.Style; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; -using Dalamud.Logging; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.UI; using ImGuiNET; -using ImGuiScene; + using ImPlotNET; using PInvoke; using Serilog.Events; @@ -48,9 +47,11 @@ internal class DalamudInterface : IDisposable, IServiceType private const float CreditsDarkeningMaxAlpha = 0.8f; private static readonly ModuleLog Log = new("DUI"); - + + private readonly Dalamud dalamud; private readonly DalamudConfiguration configuration; - + private readonly InterfaceManager interfaceManager; + private readonly ChangelogWindow changelogWindow; private readonly ColorDemoWindow colorDemoWindow; private readonly ComponentDemoWindow componentDemoWindow; @@ -92,11 +93,16 @@ internal class DalamudInterface : IDisposable, IServiceType DalamudConfiguration configuration, InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, PluginImageCache pluginImageCache, - Branding branding) + Branding branding, + Game.Framework framework, + ClientState clientState, + TitleScreenMenu titleScreenMenu, + GameGui gameGui) { + this.dalamud = dalamud; this.configuration = configuration; + this.interfaceManager = interfaceManagerWithScene.Manager; - var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); this.colorDemoWindow = new ColorDemoWindow() { IsOpen = false }; @@ -104,13 +110,20 @@ internal class DalamudInterface : IDisposable, IServiceType this.dataWindow = new DataWindow() { IsOpen = false }; this.gamepadModeNotifierWindow = new GamepadModeNotifierWindow() { IsOpen = false }; this.imeWindow = new ImeWindow() { IsOpen = false }; - this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; + this.consoleWindow = new ConsoleWindow(configuration) { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; - this.pluginWindow = new PluginInstallerWindow(pluginImageCache) { IsOpen = false }; + this.pluginWindow = new PluginInstallerWindow(pluginImageCache, configuration) { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; - this.titleScreenMenuWindow = new TitleScreenMenuWindow() { IsOpen = false }; + this.titleScreenMenuWindow = new TitleScreenMenuWindow( + clientState, + dalamud, + configuration, + framework, + gameGui, + this.interfaceManager, + titleScreenMenu) { IsOpen = false }; this.changelogWindow = new ChangelogWindow(this.titleScreenMenuWindow) { IsOpen = false }; this.profilerWindow = new ProfilerWindow() { IsOpen = false }; this.branchSwitcherWindow = new BranchSwitcherWindow() { IsOpen = false }; @@ -136,7 +149,7 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = configuration.AssertsEnabledAtStartup; this.isImGuiDrawDevMenu = this.isImGuiDrawDevMenu || configuration.DevBarOpenAtStartup; - interfaceManager.Draw += this.OnDraw; + this.interfaceManager.Draw += this.OnDraw; var tsm = Service.Get(); tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller()); @@ -173,7 +186,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// public void Dispose() { - Service.Get().Draw -= this.OnDraw; + this.interfaceManager.Draw -= this.OnDraw; this.WindowSystem.RemoveAllWindows(); @@ -356,7 +369,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// Toggles the . /// /// The data kind to switch to after opening. - public void ToggleDataWindow(string dataKind = null) + public void ToggleDataWindow(string? dataKind = null) { this.dataWindow.Toggle(); if (dataKind != null && this.dataWindow.IsOpen) @@ -378,7 +391,7 @@ internal class DalamudInterface : IDisposable, IServiceType /// /// Toggles the . /// - public void ToggleIMEWindow() => this.imeWindow.Toggle(); + public void ToggleImeWindow() => this.imeWindow.Toggle(); /// /// Toggles the . @@ -504,7 +517,8 @@ internal class DalamudInterface : IDisposable, IServiceType private void DrawCreditsDarkeningAnimation() { - using var style = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding | ImGuiStyleVar.WindowBorderSize, 0f); + using var style1 = ImRaii.PushStyle(ImGuiStyleVar.WindowRounding, 0f); + using var style2 = ImRaii.PushStyle(ImGuiStyleVar.WindowBorderSize, 0f); using var color = ImRaii.PushColor(ImGuiCol.WindowBg, new Vector4(0, 0, 0, 0)); ImGui.SetNextWindowPos(new Vector2(0, 0)); @@ -579,18 +593,16 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.BeginMainMenuBar()) { - var dalamud = Service.Get(); - var configuration = Service.Get(); var pluginManager = Service.Get(); if (ImGui.BeginMenu("Dalamud")) { ImGui.MenuItem("Draw dev menu", string.Empty, ref this.isImGuiDrawDevMenu); - var devBarAtStartup = configuration.DevBarOpenAtStartup; + var devBarAtStartup = this.configuration.DevBarOpenAtStartup; if (ImGui.MenuItem("Draw dev menu at startup", string.Empty, ref devBarAtStartup)) { - configuration.DevBarOpenAtStartup ^= true; - configuration.QueueSave(); + this.configuration.DevBarOpenAtStartup ^= true; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -607,25 +619,25 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel)) { EntryPoint.LogLevelSwitch.MinimumLevel = logLevel; - configuration.LogLevel = logLevel; - configuration.QueueSave(); + this.configuration.LogLevel = logLevel; + this.configuration.QueueSave(); } } ImGui.EndMenu(); } - var logSynchronously = configuration.LogSynchronously; + var logSynchronously = this.configuration.LogSynchronously; if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) { - configuration.LogSynchronously = logSynchronously; - configuration.QueueSave(); + this.configuration.LogSynchronously = logSynchronously; + this.configuration.QueueSave(); EntryPoint.InitLogging( - dalamud.StartInfo.LogPath!, - dalamud.StartInfo.BootShowConsole, - configuration.LogSynchronously, - dalamud.StartInfo.LogName); + this.dalamud.StartInfo.LogPath!, + this.dalamud.StartInfo.BootShowConsole, + this.configuration.LogSynchronously, + this.dalamud.StartInfo.LogName); } var antiDebug = Service.Get(); @@ -637,8 +649,8 @@ internal class DalamudInterface : IDisposable, IServiceType else antiDebug.Disable(); - configuration.IsAntiAntiDebugEnabled = newEnabled; - configuration.QueueSave(); + this.configuration.IsAntiAntiDebugEnabled = newEnabled; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -730,10 +742,10 @@ internal class DalamudInterface : IDisposable, IServiceType } } - if (ImGui.MenuItem("Report crashes at shutdown", null, configuration.ReportShutdownCrashes)) + if (ImGui.MenuItem("Report crashes at shutdown", null, this.configuration.ReportShutdownCrashes)) { - configuration.ReportShutdownCrashes = !configuration.ReportShutdownCrashes; - configuration.QueueSave(); + this.configuration.ReportShutdownCrashes = !this.configuration.ReportShutdownCrashes; + this.configuration.QueueSave(); } ImGui.Separator(); @@ -744,7 +756,7 @@ internal class DalamudInterface : IDisposable, IServiceType } ImGui.MenuItem(Util.AssemblyVersion, false); - ImGui.MenuItem(dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); + ImGui.MenuItem(this.dalamud.StartInfo.GameVersion?.ToString() ?? "Unknown version", false); ImGui.MenuItem($"D: {Util.GetGitHash()}[{Util.GetGitCommitCount()}] CS: {Util.GetGitHashClientStructs()}[{FFXIVClientStructs.Interop.Resolver.Version}]", false); ImGui.MenuItem($"CLR: {Environment.Version}", false); @@ -766,10 +778,10 @@ internal class DalamudInterface : IDisposable, IServiceType ImGuiManagedAsserts.AssertsEnabled = val; } - if (ImGui.MenuItem("Enable asserts at startup", null, configuration.AssertsEnabledAtStartup)) + if (ImGui.MenuItem("Enable asserts at startup", null, this.configuration.AssertsEnabledAtStartup)) { - configuration.AssertsEnabledAtStartup = !configuration.AssertsEnabledAtStartup; - configuration.QueueSave(); + this.configuration.AssertsEnabledAtStartup = !this.configuration.AssertsEnabledAtStartup; + this.configuration.QueueSave(); } if (ImGui.MenuItem("Clear focus")) @@ -779,7 +791,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (ImGui.MenuItem("Clear stacks")) { - Service.Get().ClearStacks(); + this.interfaceManager.ClearStacks(); } if (ImGui.MenuItem("Dump style")) @@ -792,7 +804,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (propertyInfo.PropertyType == typeof(Vector2)) { - var vec2 = (Vector2)propertyInfo.GetValue(style); + var vec2 = (Vector2)propertyInfo.GetValue(style)!; info += $"{propertyInfo.Name} = new Vector2({vec2.X.ToString(enCulture)}f, {vec2.Y.ToString(enCulture)}f),\n"; } else @@ -815,9 +827,9 @@ internal class DalamudInterface : IDisposable, IServiceType Log.Information(info); } - if (ImGui.MenuItem("Show dev bar info", null, configuration.ShowDevBarInfo)) + if (ImGui.MenuItem("Show dev bar info", null, this.configuration.ShowDevBarInfo)) { - configuration.ShowDevBarInfo = !configuration.ShowDevBarInfo; + this.configuration.ShowDevBarInfo = !this.configuration.ShowDevBarInfo; } ImGui.EndMenu(); @@ -827,7 +839,7 @@ internal class DalamudInterface : IDisposable, IServiceType { if (ImGui.MenuItem("Replace ExceptionHandler")) { - dalamud.ReplaceExceptionHandler(); + this.dalamud.ReplaceExceptionHandler(); } ImGui.EndMenu(); @@ -922,7 +934,7 @@ internal class DalamudInterface : IDisposable, IServiceType if (Service.Get().GameUiHidden) ImGui.BeginMenu("UI is hidden...", false); - if (configuration.ShowDevBarInfo) + if (this.configuration.ShowDevBarInfo) { ImGui.PushFont(InterfaceManager.MonoFont); @@ -931,9 +943,9 @@ internal class DalamudInterface : IDisposable, IServiceType ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"W:{Util.FormatBytes(GC.GetTotalMemory(false))}", false); - var videoMem = Service.Get().GetD3dMemoryInfo(); + var videoMem = this.interfaceManager.GetD3dMemoryInfo(); ImGui.BeginMenu( - !videoMem.HasValue ? $"V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", + !videoMem.HasValue ? "V:???" : $"V:{Util.FormatBytes(videoMem.Value.Used)}", false); ImGui.PopFont(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 9de87c6e3..c666a96a9 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -1285,7 +1285,7 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Represents an instance of InstanceManager with scene ready for use. /// - [ServiceManager.Service] + [ServiceManager.ProvidedService] public class InterfaceManagerWithScene : IServiceType { /// diff --git a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs index 4d1a8b5f0..e3f318223 100644 --- a/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ChangelogWindow.cs @@ -67,7 +67,7 @@ internal sealed class ChangelogWindow : Window, IDisposable // If we are going to show a changelog, make sure we have the font ready, otherwise it will hitch if (WarrantsChangelog()) - this.MakeFont(); + Service.GetAsync().ContinueWith(t => this.MakeFont(t.Result)); } private enum State @@ -98,7 +98,7 @@ internal sealed class ChangelogWindow : Window, IDisposable Service.Get().SetCreditsDarkeningAnimation(true); this.tsmWindow.AllowDrawing = false; - this.MakeFont(); + this.MakeFont(Service.Get()); this.state = State.WindowFadeIn; this.windowFade.Reset(); @@ -379,12 +379,6 @@ internal sealed class ChangelogWindow : Window, IDisposable this.logoTexture.Dispose(); } - private void MakeFont() - { - if (this.bannerFont == null) - { - var gfm = Service.Get(); - this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); - } - } + private void MakeFont(GameFontManager gfm) => + this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18)); } diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 63045ed36..b285520d4 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -56,11 +56,10 @@ internal class ConsoleWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public ConsoleWindow() + /// An instance of . + public ConsoleWindow(DalamudConfiguration configuration) : base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse) { - var configuration = Service.Get(); - this.autoScroll = configuration.LogAutoScroll; this.autoOpen = configuration.LogOpenAtStartup; SerilogEventSink.Instance.LogLine += this.OnLogLine; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 687526c9a..4233c169b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -69,7 +69,7 @@ internal class PluginInstallerWindow : Window, IDisposable private string[] testerImagePaths = new string[5]; private string testerIconPath = string.Empty; - private IDalamudTextureWrap?[] testerImages; + private IDalamudTextureWrap?[]? testerImages; private IDalamudTextureWrap? testerIcon; private bool testerError = false; @@ -132,9 +132,10 @@ internal class PluginInstallerWindow : Window, IDisposable /// Initializes a new instance of the class. /// /// An instance of class. - public PluginInstallerWindow(PluginImageCache imageCache) + /// An instance of . + public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration) : base( - Locs.WindowTitle + (Service.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", + Locs.WindowTitle + (configuration.DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) { this.IsOpen = true; diff --git a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs index a4ad62f4f..4034695e5 100644 --- a/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs +++ b/Dalamud/Interface/Internal/Windows/TitleScreenMenuWindow.cs @@ -12,8 +12,8 @@ using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Services; + using ImGuiNET; -using ImGuiScene; namespace Dalamud.Interface.Internal.Windows; @@ -25,6 +25,12 @@ internal class TitleScreenMenuWindow : Window, IDisposable private const float TargetFontSizePt = 18f; private const float TargetFontSizePx = TargetFontSizePt * 4 / 3; + private readonly ClientState clientState; + private readonly DalamudConfiguration configuration; + private readonly Framework framework; + private readonly GameGui gameGui; + private readonly TitleScreenMenu titleScreenMenu; + private readonly IDalamudTextureWrap shadeTexture; private readonly Dictionary shadeEasings = new(); @@ -39,12 +45,32 @@ internal class TitleScreenMenuWindow : Window, IDisposable /// /// Initializes a new instance of the class. /// - public TitleScreenMenuWindow() + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + /// An instance of . + public TitleScreenMenuWindow( + ClientState clientState, + Dalamud dalamud, + DalamudConfiguration configuration, + Framework framework, + GameGui gameGui, + InterfaceManager interfaceManager, + TitleScreenMenu titleScreenMenu) : base( "TitleScreenMenuOverlay", ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoBackground | ImGuiWindowFlags.NoFocusOnAppearing | ImGuiWindowFlags.NoNavFocus) { + this.clientState = clientState; + this.configuration = configuration; + this.framework = framework; + this.gameGui = gameGui; + this.titleScreenMenu = titleScreenMenu; + this.IsOpen = true; this.DisableWindowSounds = true; this.ForceMainWindow = true; @@ -53,17 +79,13 @@ internal class TitleScreenMenuWindow : Window, IDisposable this.PositionCondition = ImGuiCond.Always; this.RespectCloseHotkey = false; - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); - var shadeTex = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmShade.png")); this.shadeTexture = shadeTex ?? throw new Exception("Could not load TSM background texture."); - var framework = Service.Get(); framework.Update += this.FrameworkOnUpdate; } - + private enum State { Hide, @@ -95,8 +117,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable public void Dispose() { this.shadeTexture.Dispose(); - var framework = Service.Get(); - framework.Update -= this.FrameworkOnUpdate; + this.framework.Update -= this.FrameworkOnUpdate; } /// @@ -106,9 +127,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable return; var scale = ImGui.GetIO().FontGlobalScale; - var entries = Service.Get().Entries - .OrderByDescending(x => x.IsInternal) - .ToList(); + var entries = this.titleScreenMenu.Entries.OrderByDescending(x => x.IsInternal).ToList(); switch (this.state) { @@ -369,17 +388,14 @@ internal class TitleScreenMenuWindow : Window, IDisposable private void FrameworkOnUpdate(IFramework framework) { - var clientState = Service.Get(); - this.IsOpen = !clientState.IsLoggedIn; + this.IsOpen = !this.clientState.IsLoggedIn; - var configuration = Service.Get(); - if (!configuration.ShowTsm) + if (!this.configuration.ShowTsm) this.IsOpen = false; - var gameGui = Service.Get(); - var charaSelect = gameGui.GetAddonByName("CharaSelect", 1); - var charaMake = gameGui.GetAddonByName("CharaMake", 1); - var titleDcWorldMap = gameGui.GetAddonByName("TitleDCWorldMap", 1); + var charaSelect = this.gameGui.GetAddonByName("CharaSelect", 1); + var charaMake = this.gameGui.GetAddonByName("CharaMake", 1); + var titleDcWorldMap = this.gameGui.GetAddonByName("TitleDCWorldMap", 1); if (charaMake != IntPtr.Zero || charaSelect != IntPtr.Zero || titleDcWorldMap != IntPtr.Zero) this.IsOpen = false; } diff --git a/Dalamud/IoC/Internal/ServiceContainer.cs b/Dalamud/IoC/Internal/ServiceContainer.cs index ce7ce25a1..5b141979e 100644 --- a/Dalamud/IoC/Internal/ServiceContainer.cs +++ b/Dalamud/IoC/Internal/ServiceContainer.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -16,7 +15,7 @@ namespace Dalamud.IoC.Internal; /// This is only used to resolve dependencies for plugins. /// Dalamud services are constructed via Service{T}.ConstructObject at the moment. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] internal class ServiceContainer : IServiceProvider, IServiceType { private static readonly ModuleLog Log = new("SERVICECONTAINER"); @@ -228,7 +227,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType if (this.interfaceToTypeMap.TryGetValue(serviceType, out var implementingType)) serviceType = implementingType; - if (serviceType.GetCustomAttribute() != null) + if (serviceType.GetCustomAttribute() != null) { if (scope == null) { @@ -299,7 +298,7 @@ internal class ServiceContainer : IServiceProvider, IServiceType var contains = types.Any(x => x.IsAssignableTo(type)); // Scoped services are created on-demand - return contains || type.GetCustomAttribute() != null; + return contains || type.GetCustomAttribute() != null; } var parameters = ctor.GetParameters(); diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9a651c64e..363d01f26 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; @@ -40,7 +39,7 @@ namespace Dalamud.Plugin.Internal; /// Class responsible for loading and unloading plugins. /// NOTE: ALL plugin exposed services are marked as dependencies for PluginManager in Service{T}. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] #pragma warning disable SA1015 // DalamudTextureWrap registers textures to dispose with IM @@ -85,6 +84,9 @@ internal partial class PluginManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly HappyHttpClient happyHttpClient = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGui = Service.Get(); + static PluginManager() { DalamudApiLevel = typeof(PluginManager).Assembly.GetName().Version!.Major; @@ -131,12 +133,13 @@ internal partial class PluginManager : IDisposable, IServiceType throw new InvalidDataException("Couldn't deserialize banned plugins manifest."); } - this.openInstallerWindowPluginChangelogsLink = Service.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) => + this.openInstallerWindowPluginChangelogsLink = this.chatGui.AddChatLinkHandler("Dalamud", 1003, (_, _) => { Service.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs); }); - this.configuration.PluginTestingOptIns ??= new List(); + this.configuration.PluginTestingOptIns ??= new(); + this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient); this.ApplyPatches(); } @@ -199,6 +202,11 @@ internal partial class PluginManager : IDisposable, IServiceType } } + /// + /// Gets the main repository. + /// + public PluginRepository MainRepo { get; } + /// /// Gets a list of all plugin repositories. The main repo should always be first. /// @@ -284,11 +292,9 @@ internal partial class PluginManager : IDisposable, IServiceType /// The header text to send to chat prior to any update info. public void PrintUpdatedPlugins(List? updateMetadata, string header) { - var chatGui = Service.Get(); - if (updateMetadata is { Count: > 0 }) { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = new SeString(new List() { @@ -307,11 +313,11 @@ internal partial class PluginManager : IDisposable, IServiceType { if (metadata.Status == PluginUpdateStatus.StatusKind.Success) { - chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); + this.chatGui.Print(Locs.DalamudPluginUpdateSuccessful(metadata.Name, metadata.Version)); } else { - chatGui.Print(new XivChatEntry + this.chatGui.Print(new XivChatEntry { Message = Locs.DalamudPluginUpdateFailed(metadata.Name, metadata.Version, PluginUpdateStatus.LocalizeUpdateStatusKind(metadata.Status)), Type = XivChatType.Urgent, @@ -407,10 +413,10 @@ internal partial class PluginManager : IDisposable, IServiceType /// A representing the asynchronous operation. public async Task SetPluginReposFromConfigAsync(bool notify) { - var repos = new List() { PluginRepository.MainRepo }; + var repos = new List { this.MainRepo }; repos.AddRange(this.configuration.ThirdRepoList .Where(repo => repo.IsEnabled) - .Select(repo => new PluginRepository(repo.Url, repo.IsEnabled))); + .Select(repo => new PluginRepository(this.happyHttpClient, repo.Url, repo.IsEnabled))); this.Repos = repos; await this.ReloadPluginMastersAsync(notify); diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index 5d132fd9c..91f1625a7 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -267,10 +267,6 @@ internal class LocalPlugin : IDisposable var pluginManager = await Service.GetAsync(); var dalamud = await Service.GetAsync(); - // UiBuilder constructor requires the following two. - await Service.GetAsync(); - await Service.GetAsync(); - if (this.manifest.LoadRequiredState == 0) _ = await Service.GetAsync(); diff --git a/Dalamud/Plugin/Internal/Types/PluginRepository.cs b/Dalamud/Plugin/Internal/Types/PluginRepository.cs index 3bf67ecd7..18c528910 100644 --- a/Dalamud/Plugin/Internal/Types/PluginRepository.cs +++ b/Dalamud/Plugin/Internal/Types/PluginRepository.cs @@ -28,47 +28,44 @@ internal class PluginRepository private static readonly ModuleLog Log = new("PLUGINR"); - private static readonly HttpClient HttpClient = new(new SocketsHttpHandler - { - AutomaticDecompression = DecompressionMethods.All, - ConnectCallback = Service.Get().SharedHappyEyeballsCallback.ConnectCallback, - }) - { - Timeout = TimeSpan.FromSeconds(20), - DefaultRequestHeaders = - { - Accept = - { - new MediaTypeWithQualityHeaderValue("application/json"), - }, - CacheControl = new CacheControlHeaderValue - { - NoCache = true, - }, - UserAgent = - { - new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), - }, - }, - }; + private readonly HttpClient httpClient; /// /// Initializes a new instance of the class. /// + /// An instance of . /// The plugin master URL. /// Whether the plugin repo is enabled. - public PluginRepository(string pluginMasterUrl, bool isEnabled) + public PluginRepository(HappyHttpClient happyHttpClient, string pluginMasterUrl, bool isEnabled) { + this.httpClient = new(new SocketsHttpHandler + { + AutomaticDecompression = DecompressionMethods.All, + ConnectCallback = happyHttpClient.SharedHappyEyeballsCallback.ConnectCallback, + }) + { + Timeout = TimeSpan.FromSeconds(20), + DefaultRequestHeaders = + { + Accept = + { + new MediaTypeWithQualityHeaderValue("application/json"), + }, + CacheControl = new CacheControlHeaderValue + { + NoCache = true, + }, + UserAgent = + { + new ProductInfoHeaderValue("Dalamud", Util.AssemblyVersion), + }, + }, + }; this.PluginMasterUrl = pluginMasterUrl; this.IsThirdParty = pluginMasterUrl != MainRepoUrl; this.IsEnabled = isEnabled; } - /// - /// Gets a new instance of the class for the main repo. - /// - public static PluginRepository MainRepo => new(MainRepoUrl, true); - /// /// Gets the pluginmaster.json URL. /// @@ -94,6 +91,14 @@ internal class PluginRepository /// public PluginRepositoryState State { get; private set; } + /// + /// Gets a new instance of the class for the main repo. + /// + /// An instance of . + /// The new instance of main repository. + public static PluginRepository CreateMainRepo(HappyHttpClient happyHttpClient) => + new(happyHttpClient, MainRepoUrl, true); + /// /// Reload the plugin master asynchronously in a task. /// @@ -107,7 +112,7 @@ internal class PluginRepository { Log.Information($"Fetching repo: {this.PluginMasterUrl}"); - using var response = await HttpClient.GetAsync(this.PluginMasterUrl); + using var response = await this.httpClient.GetAsync(this.PluginMasterUrl); response.EnsureSuccessStatusCode(); var data = await response.Content.ReadAsStringAsync(); diff --git a/Dalamud/Plugin/Ipc/Internal/DataShare.cs b/Dalamud/Plugin/Ipc/Internal/DataShare.cs index 5d0faabda..a3e314b80 100644 --- a/Dalamud/Plugin/Ipc/Internal/DataShare.cs +++ b/Dalamud/Plugin/Ipc/Internal/DataShare.cs @@ -13,7 +13,7 @@ namespace Dalamud.Plugin.Ipc.Internal; /// /// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC. /// -[ServiceManager.EarlyLoadedService] +[ServiceManager.BlockingEarlyLoadedService] internal class DataShare : IServiceType { private readonly Dictionary caches = new(); diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 453fa3530..46a6ba509 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading; @@ -29,9 +30,17 @@ internal static class ServiceManager /// public static readonly ModuleLog Log = new("SVC"); - private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); +#if DEBUG + /// + /// Marks which service constructor the current thread's in. For use from only. + /// + internal static readonly ThreadLocal CurrentConstructorServiceType = new(); + [SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")] private static readonly List LoadedServices = new(); +#endif + + private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); private static ManualResetEvent unloadResetEvent = new(false); @@ -86,21 +95,34 @@ internal static class ServiceManager /// Instance of . public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner) { +#if DEBUG lock (LoadedServices) { - void ProvideService(T service) where T : IServiceType - { - Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); - Service.Provide(service); - LoadedServices.Add(typeof(T)); - } - ProvideService(dalamud); ProvideService(fs); ProvideService(configuration); ProvideService(new ServiceContainer()); ProvideService(scanner); } + + return; + + void ProvideService(T service) where T : IServiceType + { + Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute"); + Service.Provide(service); + LoadedServices.Add(typeof(T)); + } +#else + ProvideService(dalamud); + ProvideService(fs); + ProvideService(configuration); + ProvideService(new ServiceContainer()); + ProvideService(scanner); + return; + + void ProvideService(T service) where T : IServiceType => Service.Provide(service); +#endif } /// @@ -171,7 +193,22 @@ internal static class ServiceManager { try { - await Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + var whenBlockingComplete = Task.WhenAll(blockingEarlyLoadingServices.Select(x => getAsyncTaskMap[x])); + while (await Task.WhenAny(whenBlockingComplete, Task.Delay(30000)) != whenBlockingComplete) + { + if (NativeFunctions.MessageBoxW( + IntPtr.Zero, + "Dalamud is taking a long time to load. Would you like to continue without Dalamud?\n" + + "This can be caused by a faulty plugin, or a bug in Dalamud.", + "Dalamud", + NativeFunctions.MessageBoxType.IconWarning | NativeFunctions.MessageBoxType.YesNo) == 6) + { + throw new TimeoutException( + "Failed to load services in the given time limit, " + + "and the user chose to continue without Dalamud."); + } + } + BlockingServicesLoadedTaskCompletionSource.SetResult(); Timings.Event("BlockingServices Initialized"); } @@ -215,13 +252,14 @@ internal static class ServiceManager tasks.Add((Task)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( - "StartLoader", + nameof(Service.StartLoader), BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, null, null, null)); servicesToLoad.Remove(serviceType); +#if DEBUG tasks.Add(tasks.Last().ContinueWith(task => { if (task.IsFaulted) @@ -231,6 +269,7 @@ internal static class ServiceManager LoadedServices.Add(serviceType); } })); +#endif } if (!tasks.Any()) @@ -350,10 +389,12 @@ internal static class ServiceManager null); } +#if DEBUG lock (LoadedServices) { LoadedServices.Clear(); } +#endif unloadResetEvent.Set(); } @@ -373,7 +414,7 @@ internal static class ServiceManager /// The type of service this type is. public static ServiceKind GetServiceKind(this Type type) { - var attr = type.GetCustomAttribute(true)?.GetType(); + var attr = type.GetCustomAttribute(true)?.GetType(); if (attr == null) return ServiceKind.None; @@ -381,13 +422,13 @@ internal static class ServiceManager type.IsAssignableTo(typeof(IServiceType)), "Service did not inherit from IServiceType"); - if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) + if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedServiceAttribute))) return ServiceKind.BlockingEarlyLoadedService; - if (attr.IsAssignableTo(typeof(EarlyLoadedService))) + if (attr.IsAssignableTo(typeof(EarlyLoadedServiceAttribute))) return ServiceKind.EarlyLoadedService; - if (attr.IsAssignableTo(typeof(ScopedService))) + if (attr.IsAssignableTo(typeof(ScopedServiceAttribute))) return ServiceKind.ScopedService; return ServiceKind.ProvidedService; @@ -414,16 +455,57 @@ internal static class ServiceManager /// Indicates that the class is a service. /// [AttributeUsage(AttributeTargets.Class)] - public class Service : Attribute + public abstract class ServiceAttribute : Attribute { + /// + /// Initializes a new instance of the class. + /// + /// The kind of the service. + protected ServiceAttribute(ServiceKind kind) => this.Kind = kind; + + /// + /// Gets the kind of the service. + /// + public ServiceKind Kind { get; } + } + + /// + /// Indicates that the class is a service, that is provided by some other source. + /// + [AttributeUsage(AttributeTargets.Class)] + public class ProvidedServiceAttribute : ServiceAttribute + { + /// + /// Initializes a new instance of the class. + /// + public ProvidedServiceAttribute() + : base(ServiceKind.ProvidedService) + { + } } /// /// Indicates that the class is a service, and will be instantiated automatically on startup. /// [AttributeUsage(AttributeTargets.Class)] - public class EarlyLoadedService : Service + public class EarlyLoadedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public EarlyLoadedServiceAttribute() + : this(ServiceKind.EarlyLoadedService) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The service kind. + protected EarlyLoadedServiceAttribute(ServiceKind kind) + : base(kind) + { + } } /// @@ -431,8 +513,15 @@ internal static class ServiceManager /// blocking game main thread until it completes. /// [AttributeUsage(AttributeTargets.Class)] - public class BlockingEarlyLoadedService : EarlyLoadedService + public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public BlockingEarlyLoadedServiceAttribute() + : base(ServiceKind.BlockingEarlyLoadedService) + { + } } /// @@ -440,8 +529,15 @@ internal static class ServiceManager /// service scope, and that it cannot be created outside of a scope. /// [AttributeUsage(AttributeTargets.Class)] - public class ScopedService : Service + public class ScopedServiceAttribute : ServiceAttribute { + /// + /// Initializes a new instance of the class. + /// + public ScopedServiceAttribute() + : base(ServiceKind.ScopedService) + { + } } /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index b609c9082..9c7f0411d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -1,6 +1,5 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; using System.Threading.Tasks; @@ -20,17 +19,26 @@ namespace Dalamud; /// Only used internally within Dalamud, if plugins need access to things it should be _only_ via DI. /// /// The class you want to store in the service locator. +[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")] internal static class Service where T : IServiceType { + private static readonly ServiceManager.ServiceAttribute ServiceAttribute; private static TaskCompletionSource instanceTcs = new(); + private static List? dependencyServices; static Service() { - var exposeToPlugins = typeof(T).GetCustomAttribute() != null; + var type = typeof(T); + ServiceAttribute = + type.GetCustomAttribute(true) + ?? throw new InvalidOperationException( + $"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations."); + + var exposeToPlugins = type.GetCustomAttribute() != null; if (exposeToPlugins) - ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called; will be exposed to plugins", type.Name); else - ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); + ServiceManager.Log.Debug("Service<{0}>: Static ctor called", type.Name); if (exposeToPlugins) Service.Get().RegisterSingleton(instanceTcs.Task); @@ -63,8 +71,8 @@ internal static class Service where T : IServiceType /// Object to set. public static void Provide(T obj) { - instanceTcs.SetResult(obj); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + instanceTcs.SetResult(obj); } /// @@ -83,6 +91,21 @@ internal static class Service where T : IServiceType /// The object. public static T Get() { +#if DEBUG + if (ServiceAttribute.Kind != ServiceManager.ServiceKind.ProvidedService + && ServiceManager.CurrentConstructorServiceType.Value is { } currentServiceType) + { + var deps = ServiceHelpers.GetDependencies(currentServiceType); + if (!deps.Contains(typeof(T))) + { + throw new InvalidOperationException( + $"Calling {nameof(Service)}<{typeof(T)}>.{nameof(Get)} which is not one of the" + + $" dependency services is forbidden from the service constructor of {currentServiceType}." + + $" This has a high chance of introducing hard-to-debug hangs."); + } + } +#endif + if (!instanceTcs.Task.IsCompleted) instanceTcs.Task.Wait(); return instanceTcs.Task.Result; @@ -116,12 +139,16 @@ internal static class Service where T : IServiceType } /// - /// Gets an enumerable containing Service<T>s that are required for this Service to initialize without blocking. + /// Gets an enumerable containing s that are required for this Service to initialize + /// without blocking. /// /// List of dependency services. [UsedImplicitly] public static List GetDependencyServices() { + if (dependencyServices is not null) + return dependencyServices; + var res = new List(); ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name); @@ -189,19 +216,42 @@ internal static class Service where T : IServiceType ServiceManager.Log.Verbose("Service<{0}>: => Dependency: {1}", typeof(T).Name, type.Name); } - return res - .Distinct() - .ToList(); + var deps = res + .Distinct() + .ToList(); + if (typeof(T).GetCustomAttribute() is not null) + { + var offenders = deps.Where( + x => x.GetCustomAttribute(true)!.Kind + is not ServiceManager.ServiceKind.BlockingEarlyLoadedService + and not ServiceManager.ServiceKind.ProvidedService) + .ToArray(); + if (offenders.Any()) + { + ServiceManager.Log.Error( + "{me} is a {bels}; it can only depend on {bels} and {ps}.\nOffending dependencies:\n{offenders}", + typeof(T), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.BlockingEarlyLoadedServiceAttribute), + nameof(ServiceManager.ProvidedServiceAttribute), + string.Join("\n", offenders.Select(x => $"\t* {x.Name}"))); + } + } + + return dependencyServices = deps; } - [UsedImplicitly] - private static Task StartLoader() + /// + /// Starts the service loader. Only to be called from . + /// + /// The loader task. + internal static Task StartLoader() { if (instanceTcs.Task.IsCompleted) throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed."); - var attr = typeof(T).GetCustomAttribute(true)?.GetType(); - if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) + var attr = ServiceAttribute.GetType(); + if (attr.IsAssignableTo(typeof(ServiceManager.EarlyLoadedServiceAttribute)) != true) throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); return Task.Run(Timings.AttachTimingHandle(async () => @@ -212,6 +262,7 @@ internal static class Service where T : IServiceType var instance = await ConstructObject(); instanceTcs.SetResult(instance); + List? tasks = null; foreach (var method in typeof(T).GetMethods( BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { @@ -221,9 +272,24 @@ internal static class Service where T : IServiceType ServiceManager.Log.Debug("Service<{0}>: Calling {1}", typeof(T).Name, method.Name); var args = await Task.WhenAll(method.GetParameters().Select( x => ResolveServiceFromTypeAsync(x.ParameterType))); - method.Invoke(instance, args); + try + { + if (method.Invoke(instance, args) is Task task) + { + tasks ??= new(); + tasks.Add(task); + } + } + catch (Exception e) + { + tasks ??= new(); + tasks.Add(Task.FromException(e)); + } } + if (tasks is not null) + await Task.WhenAll(tasks); + ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name); return instance; } @@ -303,7 +369,19 @@ internal static class Service where T : IServiceType ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType))); using (Timings.Start($"{typeof(T).Name} Construct")) { +#if DEBUG + ServiceManager.CurrentConstructorServiceType.Value = typeof(Service); + try + { + return (T)ctor.Invoke(args)!; + } + finally + { + ServiceManager.CurrentConstructorServiceType.Value = null; + } +#else return (T)ctor.Invoke(args)!; +#endif } } @@ -328,30 +406,43 @@ internal static class Service where T : IServiceType internal static class ServiceHelpers { /// - /// Get a list of dependencies for a service. Only accepts Service<T> types. - /// These are returned as Service<T> types. + /// Get a list of dependencies for a service. Only accepts types. + /// These are returned as types. /// /// The dependencies for this service. /// A list of dependencies. public static List GetDependencies(Type serviceType) { +#if DEBUG + if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>)) + { + throw new ArgumentException( + $"Expected an instance of {nameof(Service)}<>", + nameof(serviceType)); + } +#endif + return (List)serviceType.InvokeMember( - "GetDependencyServices", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null) ?? new List(); + nameof(Service.GetDependencyServices), + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + null) ?? new List(); } /// - /// Get the Service<T> type for a given service type. + /// Get the type for a given service type. /// This will throw if the service type is not a valid service. /// - /// The type to obtain a Service<T> for. - /// The Service<T>. + /// The type to obtain a for. + /// The . public static Type GetAsService(Type type) { - return typeof(Service<>) - .MakeGenericType(type); +#if DEBUG + if (!type.IsAssignableTo(typeof(IServiceType))) + throw new ArgumentException($"Expected an instance of {nameof(IServiceType)}", nameof(type)); +#endif + + return typeof(Service<>).MakeGenericType(type); } } diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index 9feb17c0d..a013e95b5 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -21,7 +21,7 @@ namespace Dalamud.Storage; /// /// This is not an early-loaded service, as it is needed before they are initialized. /// -[ServiceManager.Service] +[ServiceManager.ProvidedService] public class ReliableFileStorage : IServiceType, IDisposable { private static readonly ModuleLog Log = new("VFS");