Better Service dependency handling (#1535)

This commit is contained in:
srkizer 2023-11-29 06:20:16 +09:00 committed by GitHub
parent fcebd15077
commit b66be84b93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 415 additions and 197 deletions

View file

@ -21,7 +21,7 @@ namespace Dalamud.Configuration.Internal;
/// Class containing Dalamud settings.
/// </summary>
[Serializable]
[ServiceManager.Service]
[ServiceManager.ProvidedService]
#pragma warning disable SA1015
[InherentDependency<ReliableFileStorage>] // We must still have this when unloading
#pragma warning restore SA1015

View file

@ -30,7 +30,7 @@ namespace Dalamud;
/// <summary>
/// The main Dalamud class containing all subsystems.
/// </summary>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
internal sealed class Dalamud : IServiceType
{
#region Internals

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Events;
/// Service provider for addon event management.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class AddonEventManager : IDisposable, IServiceType
{
/// <summary>

View file

@ -18,7 +18,7 @@ namespace Dalamud.Game.Addon.Lifecycle;
/// This class provides events for in-game addon lifecycles.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class AddonLifecycle : IDisposable, IServiceType
{
private static readonly ModuleLog Log = new("AddonLifecycle");

View file

@ -12,7 +12,7 @@ namespace Dalamud.Game.Config;
/// This class represents the game's configuration.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable
{
private readonly GameConfigAddressResolver address = new();

View file

@ -12,7 +12,7 @@ namespace Dalamud.Game.DutyState;
/// This class represents the state of the currently occupied duty.
/// </summary>
[InterfaceVersion("1.0")]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal unsafe class DutyState : IDisposable, IServiceType, IDutyState
{
private readonly DutyStateAddressResolver address;

View file

@ -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<HappyHttpClient>.Get().SharedHttpClient;
private readonly HttpClient httpClient;
/// <summary>
/// Initializes a new instance of the <see cref="UniversalisMarketBoardUploader"/> class.
/// </summary>
public UniversalisMarketBoardUploader()
{
}
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
public UniversalisMarketBoardUploader(HappyHttpClient happyHttpClient) =>
this.httpClient = happyHttpClient.SharedHttpClient;
/// <inheritdoc/>
public async Task Upload(MarketBoardItemRequest request)

View file

@ -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;
/// <summary>
/// This class handles network notifications and uploading market board data.
/// </summary>
[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);

View file

@ -11,7 +11,7 @@ namespace Dalamud.Game;
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.Service]
[ServiceManager.ProvidedService]
#pragma warning disable SA1015
[ResolveVia<ISigScanner>]
#pragma warning restore SA1015

View file

@ -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.
/// </summary>
[PluginInterface]
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
#pragma warning disable SA1015
[ResolveVia<IDragDropManager>]
#pragma warning restore SA1015

View file

@ -22,7 +22,7 @@ namespace Dalamud.Interface.GameFonts;
/// <summary>
/// Loads game font for use in ImGui.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal class GameFontManager : IServiceType
{
private static readonly string?[] FontNames =

View file

@ -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<TitleScreenMenu>.Get();
tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), branding.LogoSmall, () => this.OpenPluginInstaller());
@ -173,7 +186,7 @@ internal class DalamudInterface : IDisposable, IServiceType
/// <inheritdoc/>
public void Dispose()
{
Service<InterfaceManager>.Get().Draw -= this.OnDraw;
this.interfaceManager.Draw -= this.OnDraw;
this.WindowSystem.RemoveAllWindows();
@ -356,7 +369,7 @@ internal class DalamudInterface : IDisposable, IServiceType
/// Toggles the <see cref="DataWindow"/>.
/// </summary>
/// <param name="dataKind">The data kind to switch to after opening.</param>
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
/// <summary>
/// Toggles the <see cref="ImeWindow"/>.
/// </summary>
public void ToggleIMEWindow() => this.imeWindow.Toggle();
public void ToggleImeWindow() => this.imeWindow.Toggle();
/// <summary>
/// Toggles the <see cref="ConsoleWindow"/>.
@ -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<Dalamud>.Get();
var configuration = Service<DalamudConfiguration>.Get();
var pluginManager = Service<PluginManager>.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<AntiDebug>.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<InterfaceManager>.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<GameGui>.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<InterfaceManager>.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();

View file

@ -1285,7 +1285,7 @@ internal class InterfaceManager : IDisposable, IServiceType
/// <summary>
/// Represents an instance of InstanceManager with scene ready for use.
/// </summary>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
public class InterfaceManagerWithScene : IServiceType
{
/// <summary>

View file

@ -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<GameFontManager>.GetAsync().ContinueWith(t => this.MakeFont(t.Result));
}
private enum State
@ -98,7 +98,7 @@ internal sealed class ChangelogWindow : Window, IDisposable
Service<DalamudInterface>.Get().SetCreditsDarkeningAnimation(true);
this.tsmWindow.AllowDrawing = false;
this.MakeFont();
this.MakeFont(Service<GameFontManager>.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<GameFontManager>.Get();
this.bannerFont = gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
}
}
private void MakeFont(GameFontManager gfm) =>
this.bannerFont ??= gfm.NewFontRef(new GameFontStyle(GameFontFamilyAndSize.MiedingerMid18));
}

View file

@ -56,11 +56,10 @@ internal class ConsoleWindow : Window, IDisposable
/// <summary>
/// Initializes a new instance of the <see cref="ConsoleWindow"/> class.
/// </summary>
public ConsoleWindow()
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
public ConsoleWindow(DalamudConfiguration configuration)
: base("Dalamud Console", ImGuiWindowFlags.NoScrollbar | ImGuiWindowFlags.NoScrollWithMouse)
{
var configuration = Service<DalamudConfiguration>.Get();
this.autoScroll = configuration.LogAutoScroll;
this.autoOpen = configuration.LogOpenAtStartup;
SerilogEventSink.Instance.LogLine += this.OnLogLine;

View file

@ -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 <see cref="PluginInstallerWindow"/> class.
/// </summary>
/// <param name="imageCache">An instance of <see cref="PluginImageCache"/> class.</param>
public PluginInstallerWindow(PluginImageCache imageCache)
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
public PluginInstallerWindow(PluginImageCache imageCache, DalamudConfiguration configuration)
: base(
Locs.WindowTitle + (Service<DalamudConfiguration>.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;

View file

@ -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<Guid, InOutCubic> shadeEasings = new();
@ -39,12 +45,32 @@ internal class TitleScreenMenuWindow : Window, IDisposable
/// <summary>
/// Initializes a new instance of the <see cref="TitleScreenMenuWindow"/> class.
/// </summary>
public TitleScreenMenuWindow()
/// <param name="clientState">An instance of <see cref="ClientState"/>.</param>
/// <param name="dalamud">An instance of <see cref="Dalamud"/>.</param>
/// <param name="configuration">An instance of <see cref="DalamudConfiguration"/>.</param>
/// <param name="framework">An instance of <see cref="Framework"/>.</param>
/// <param name="interfaceManager">An instance of <see cref="InterfaceManager"/>.</param>
/// <param name="titleScreenMenu">An instance of <see cref="TitleScreenMenu"/>.</param>
/// <param name="gameGui">An instance of <see cref="gameGui"/>.</param>
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<Dalamud>.Get();
var interfaceManager = Service<InterfaceManager>.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<Framework>.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<Framework>.Get();
framework.Update -= this.FrameworkOnUpdate;
this.framework.Update -= this.FrameworkOnUpdate;
}
/// <inheritdoc/>
@ -106,9 +127,7 @@ internal class TitleScreenMenuWindow : Window, IDisposable
return;
var scale = ImGui.GetIO().FontGlobalScale;
var entries = Service<TitleScreenMenu>.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<ClientState>.Get();
this.IsOpen = !clientState.IsLoggedIn;
this.IsOpen = !this.clientState.IsLoggedIn;
var configuration = Service<DalamudConfiguration>.Get();
if (!configuration.ShowTsm)
if (!this.configuration.ShowTsm)
this.IsOpen = false;
var gameGui = Service<GameGui>.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;
}

View file

@ -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.
/// </summary>
[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<ServiceManager.ScopedService>() != null)
if (serviceType.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != 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<ServiceManager.ScopedService>() != null;
return contains || type.GetCustomAttribute<ServiceManager.ScopedServiceAttribute>() != null;
}
var parameters = ctor.GetParameters();

View file

@ -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}.
/// </summary>
[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<HappyHttpClient>.Get();
[ServiceManager.ServiceDependency]
private readonly ChatGui chatGui = Service<ChatGui>.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<ChatGui>.Get().AddChatLinkHandler("Dalamud", 1003, (_, _) =>
this.openInstallerWindowPluginChangelogsLink = this.chatGui.AddChatLinkHandler("Dalamud", 1003, (_, _) =>
{
Service<DalamudInterface>.GetNullable()?.OpenPluginInstallerTo(PluginInstallerWindow.PluginInstallerOpenKind.Changelogs);
});
this.configuration.PluginTestingOptIns ??= new List<PluginTestingOptIn>();
this.configuration.PluginTestingOptIns ??= new();
this.MainRepo = PluginRepository.CreateMainRepo(this.happyHttpClient);
this.ApplyPatches();
}
@ -199,6 +202,11 @@ internal partial class PluginManager : IDisposable, IServiceType
}
}
/// <summary>
/// Gets the main repository.
/// </summary>
public PluginRepository MainRepo { get; }
/// <summary>
/// Gets a list of all plugin repositories. The main repo should always be first.
/// </summary>
@ -284,11 +292,9 @@ internal partial class PluginManager : IDisposable, IServiceType
/// <param name="header">The header text to send to chat prior to any update info.</param>
public void PrintUpdatedPlugins(List<PluginUpdateStatus>? updateMetadata, string header)
{
var chatGui = Service<ChatGui>.Get();
if (updateMetadata is { Count: > 0 })
{
chatGui.Print(new XivChatEntry
this.chatGui.Print(new XivChatEntry
{
Message = new SeString(new List<Payload>()
{
@ -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
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task SetPluginReposFromConfigAsync(bool notify)
{
var repos = new List<PluginRepository>() { PluginRepository.MainRepo };
var repos = new List<PluginRepository> { 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);

View file

@ -267,10 +267,6 @@ internal class LocalPlugin : IDisposable
var pluginManager = await Service<PluginManager>.GetAsync();
var dalamud = await Service<Dalamud>.GetAsync();
// UiBuilder constructor requires the following two.
await Service<InterfaceManager>.GetAsync();
await Service<GameFontManager>.GetAsync();
if (this.manifest.LoadRequiredState == 0)
_ = await Service<InterfaceManager.InterfaceManagerWithScene>.GetAsync();

View file

@ -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<HappyHttpClient>.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;
/// <summary>
/// Initializes a new instance of the <see cref="PluginRepository"/> class.
/// </summary>
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
/// <param name="pluginMasterUrl">The plugin master URL.</param>
/// <param name="isEnabled">Whether the plugin repo is enabled.</param>
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;
}
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
public static PluginRepository MainRepo => new(MainRepoUrl, true);
/// <summary>
/// Gets the pluginmaster.json URL.
/// </summary>
@ -94,6 +91,14 @@ internal class PluginRepository
/// </summary>
public PluginRepositoryState State { get; private set; }
/// <summary>
/// Gets a new instance of the <see cref="PluginRepository"/> class for the main repo.
/// </summary>
/// <param name="happyHttpClient">An instance of <see cref="HappyHttpClient"/>.</param>
/// <returns>The new instance of main repository.</returns>
public static PluginRepository CreateMainRepo(HappyHttpClient happyHttpClient) =>
new(happyHttpClient, MainRepoUrl, true);
/// <summary>
/// Reload the plugin master asynchronously in a task.
/// </summary>
@ -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();

View file

@ -13,7 +13,7 @@ namespace Dalamud.Plugin.Ipc.Internal;
/// <summary>
/// This class facilitates sharing data-references of standard types between plugins without using more expensive IPC.
/// </summary>
[ServiceManager.EarlyLoadedService]
[ServiceManager.BlockingEarlyLoadedService]
internal class DataShare : IServiceType
{
private readonly Dictionary<string, DataCache> caches = new();

View file

@ -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
/// </summary>
public static readonly ModuleLog Log = new("SVC");
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
#if DEBUG
/// <summary>
/// Marks which service constructor the current thread's in. For use from <see cref="Service{T}"/> only.
/// </summary>
internal static readonly ThreadLocal<Type?> CurrentConstructorServiceType = new();
[SuppressMessage("ReSharper", "CollectionNeverQueried.Local", Justification = "Debugging purposes")]
private static readonly List<Type> LoadedServices = new();
#endif
private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new();
private static ManualResetEvent unloadResetEvent = new(false);
@ -86,21 +95,34 @@ internal static class ServiceManager
/// <param name="scanner">Instance of <see cref="TargetSigScanner"/>.</param>
public static void InitializeProvidedServices(Dalamud dalamud, ReliableFileStorage fs, DalamudConfiguration configuration, TargetSigScanner scanner)
{
#if DEBUG
lock (LoadedServices)
{
void ProvideService<T>(T service) where T : IServiceType
{
Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute");
Service<T>.Provide(service);
LoadedServices.Add(typeof(T));
}
ProvideService(dalamud);
ProvideService(fs);
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
}
return;
void ProvideService<T>(T service) where T : IServiceType
{
Debug.Assert(typeof(T).GetServiceKind().HasFlag(ServiceKind.ProvidedService), "Provided service must have Service attribute");
Service<T>.Provide(service);
LoadedServices.Add(typeof(T));
}
#else
ProvideService(dalamud);
ProvideService(fs);
ProvideService(configuration);
ProvideService(new ServiceContainer());
ProvideService(scanner);
return;
void ProvideService<T>(T service) where T : IServiceType => Service<T>.Provide(service);
#endif
}
/// <summary>
@ -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<IServiceType>.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
/// <returns>The type of service this type is.</returns>
public static ServiceKind GetServiceKind(this Type type)
{
var attr = type.GetCustomAttribute<Service>(true)?.GetType();
var attr = type.GetCustomAttribute<ServiceAttribute>(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.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class Service : Attribute
public abstract class ServiceAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceAttribute"/> class.
/// </summary>
/// <param name="kind">The kind of the service.</param>
protected ServiceAttribute(ServiceKind kind) => this.Kind = kind;
/// <summary>
/// Gets the kind of the service.
/// </summary>
public ServiceKind Kind { get; }
}
/// <summary>
/// Indicates that the class is a service, that is provided by some other source.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ProvidedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ProvidedServiceAttribute"/> class.
/// </summary>
public ProvidedServiceAttribute()
: base(ServiceKind.ProvidedService)
{
}
}
/// <summary>
/// Indicates that the class is a service, and will be instantiated automatically on startup.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class EarlyLoadedService : Service
public class EarlyLoadedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
/// </summary>
public EarlyLoadedServiceAttribute()
: this(ServiceKind.EarlyLoadedService)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="EarlyLoadedServiceAttribute"/> class.
/// </summary>
/// <param name="kind">The service kind.</param>
protected EarlyLoadedServiceAttribute(ServiceKind kind)
: base(kind)
{
}
}
/// <summary>
@ -431,8 +513,15 @@ internal static class ServiceManager
/// blocking game main thread until it completes.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class BlockingEarlyLoadedService : EarlyLoadedService
public class BlockingEarlyLoadedServiceAttribute : EarlyLoadedServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="BlockingEarlyLoadedServiceAttribute"/> class.
/// </summary>
public BlockingEarlyLoadedServiceAttribute()
: base(ServiceKind.BlockingEarlyLoadedService)
{
}
}
/// <summary>
@ -440,8 +529,15 @@ internal static class ServiceManager
/// service scope, and that it cannot be created outside of a scope.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public class ScopedService : Service
public class ScopedServiceAttribute : ServiceAttribute
{
/// <summary>
/// Initializes a new instance of the <see cref="ScopedServiceAttribute"/> class.
/// </summary>
public ScopedServiceAttribute()
: base(ServiceKind.ScopedService)
{
}
}
/// <summary>

View file

@ -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.
/// </remarks>
/// <typeparam name="T">The class you want to store in the service locator.</typeparam>
[SuppressMessage("ReSharper", "StaticMemberInGenericType", Justification = "Service container static type")]
internal static class Service<T> where T : IServiceType
{
private static readonly ServiceManager.ServiceAttribute ServiceAttribute;
private static TaskCompletionSource<T> instanceTcs = new();
private static List<Type>? dependencyServices;
static Service()
{
var exposeToPlugins = typeof(T).GetCustomAttribute<PluginInterfaceAttribute>() != null;
var type = typeof(T);
ServiceAttribute =
type.GetCustomAttribute<ServiceManager.ServiceAttribute>(true)
?? throw new InvalidOperationException(
$"{nameof(T)} is missing {nameof(ServiceManager.ServiceAttribute)} annotations.");
var exposeToPlugins = type.GetCustomAttribute<PluginInterfaceAttribute>() != 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<ServiceContainer>.Get().RegisterSingleton(instanceTcs.Task);
@ -63,8 +71,8 @@ internal static class Service<T> where T : IServiceType
/// <param name="obj">Object to set.</param>
public static void Provide(T obj)
{
instanceTcs.SetResult(obj);
ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name);
instanceTcs.SetResult(obj);
}
/// <summary>
@ -83,6 +91,21 @@ internal static class Service<T> where T : IServiceType
/// <returns>The object.</returns>
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<IServiceType>)}<{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<T> where T : IServiceType
}
/// <summary>
/// Gets an enumerable containing Service&lt;T&gt;s that are required for this Service to initialize without blocking.
/// Gets an enumerable containing <see cref="Service{T}"/>s that are required for this Service to initialize
/// without blocking.
/// </summary>
/// <returns>List of dependency services.</returns>
[UsedImplicitly]
public static List<Type> GetDependencyServices()
{
if (dependencyServices is not null)
return dependencyServices;
var res = new List<Type>();
ServiceManager.Log.Verbose("Service<{0}>: Getting dependencies", typeof(T).Name);
@ -189,19 +216,42 @@ internal static class Service<T> 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<ServiceManager.BlockingEarlyLoadedServiceAttribute>() is not null)
{
var offenders = deps.Where(
x => x.GetCustomAttribute<ServiceManager.ServiceAttribute>(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<T> StartLoader()
/// <summary>
/// Starts the service loader. Only to be called from <see cref="ServiceManager"/>.
/// </summary>
/// <returns>The loader task.</returns>
internal static Task<T> StartLoader()
{
if (instanceTcs.Task.IsCompleted)
throw new InvalidOperationException($"{typeof(T).Name} is already loaded or disposed.");
var attr = typeof(T).GetCustomAttribute<ServiceManager.Service>(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<T> where T : IServiceType
var instance = await ConstructObject();
instanceTcs.SetResult(instance);
List<Task>? tasks = null;
foreach (var method in typeof(T).GetMethods(
BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic))
{
@ -221,9 +272,24 @@ internal static class Service<T> 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<T> where T : IServiceType
ctor.GetParameters().Select(x => ResolveServiceFromTypeAsync(x.ParameterType)));
using (Timings.Start($"{typeof(T).Name} Construct"))
{
#if DEBUG
ServiceManager.CurrentConstructorServiceType.Value = typeof(Service<T>);
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<T> where T : IServiceType
internal static class ServiceHelpers
{
/// <summary>
/// Get a list of dependencies for a service. Only accepts Service&lt;T&gt; types.
/// These are returned as Service&lt;T&gt; types.
/// Get a list of dependencies for a service. Only accepts <see cref="Service{T}"/> types.
/// These are returned as <see cref="Service{T}"/> types.
/// </summary>
/// <param name="serviceType">The dependencies for this service.</param>
/// <returns>A list of dependencies.</returns>
public static List<Type> GetDependencies(Type serviceType)
{
#if DEBUG
if (!serviceType.IsGenericType || serviceType.GetGenericTypeDefinition() != typeof(Service<>))
{
throw new ArgumentException(
$"Expected an instance of {nameof(Service<IServiceType>)}<>",
nameof(serviceType));
}
#endif
return (List<Type>)serviceType.InvokeMember(
"GetDependencyServices",
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null) ?? new List<Type>();
nameof(Service<IServiceType>.GetDependencyServices),
BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public,
null,
null,
null) ?? new List<Type>();
}
/// <summary>
/// Get the Service&lt;T&gt; type for a given service type.
/// Get the <see cref="Service{T}"/> type for a given service type.
/// This will throw if the service type is not a valid service.
/// </summary>
/// <param name="type">The type to obtain a Service&lt;T&gt; for.</param>
/// <returns>The Service&lt;T&gt;.</returns>
/// <param name="type">The type to obtain a <see cref="Service{T}"/> for.</param>
/// <returns>The <see cref="Service{T}"/>.</returns>
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);
}
}

View file

@ -21,7 +21,7 @@ namespace Dalamud.Storage;
/// <remarks>
/// This is not an early-loaded service, as it is needed before they are initialized.
/// </remarks>
[ServiceManager.Service]
[ServiceManager.ProvidedService]
public class ReliableFileStorage : IServiceType, IDisposable
{
private static readonly ModuleLog Log = new("VFS");