From 716736f022c361c4e418beb150bacd6167482d79 Mon Sep 17 00:00:00 2001 From: kizer Date: Wed, 29 Jun 2022 18:51:40 +0900 Subject: [PATCH] Improvements (#903) --- Dalamud.Boot/xivfixes.cpp | 91 ++++ Dalamud.Boot/xivfixes.h | 1 + Dalamud.Injector/EntryPoint.cs | 2 +- .../Internal/DalamudConfiguration.cs | 11 + Dalamud/Dalamud.cs | 86 +--- Dalamud/EntryPoint.cs | 101 +++-- Dalamud/Game/ChatHandlers.cs | 55 +-- Dalamud/Game/ClientState/Buddy/BuddyList.cs | 11 +- Dalamud/Game/ClientState/Buddy/BuddyMember.cs | 5 +- Dalamud/Game/ClientState/ClientState.cs | 42 +- Dalamud/Game/ClientState/Fates/Fate.cs | 4 +- .../Game/ClientState/GamePad/GamepadState.cs | 2 +- .../Game/ClientState/Objects/ObjectTable.cs | 4 +- .../Game/ClientState/Objects/TargetManager.cs | 20 +- .../ClientState/Objects/Types/GameObject.cs | 4 +- Dalamud/Game/ClientState/Party/PartyList.cs | 15 +- Dalamud/Game/Command/CommandManager.cs | 12 +- Dalamud/Game/Framework.cs | 183 +++++--- Dalamud/Game/Gui/ChatGui.cs | 28 +- Dalamud/Game/Gui/ContextMenus/ContextMenu.cs | 10 +- Dalamud/Game/Gui/Dtr/DtrBar.cs | 57 +-- Dalamud/Game/Gui/FlyText/FlyTextGui.cs | 2 +- Dalamud/Game/Gui/GameGui.cs | 22 +- Dalamud/Game/Gui/Internal/DalamudIME.cs | 4 +- .../Game/Gui/PartyFinder/PartyFinderGui.cs | 2 +- Dalamud/Game/Gui/Toast/ToastGui.cs | 6 +- Dalamud/Game/Internal/DalamudAtkTweaks.cs | 47 +- Dalamud/Game/Network/GameNetwork.cs | 4 +- .../UniversalisMarketBoardUploader.cs | 12 +- .../Game/Network/Internal/NetworkHandlers.cs | 22 +- .../Game/Network/Internal/WinSockHandlers.cs | 2 +- Dalamud/Game/SigScanner.cs | 10 +- Dalamud/Hooking/Hook.cs | 23 +- .../Internal/FunctionPointerVariableHook.cs | 59 ++- Dalamud/Hooking/Internal/HookManager.cs | 5 + Dalamud/Hooking/Internal/MinHookHook.cs | 54 ++- Dalamud/Hooking/Internal/ReloadedHook.cs | 41 +- .../Interface/GameFonts/GameFontManager.cs | 7 +- .../Interface/Internal/DalamudInterface.cs | 50 ++- .../Interface/Internal/InterfaceManager.cs | 48 +- .../Internal/Windows/ConsoleWindow.cs | 5 +- .../Internal/Windows/PluginImageCache.cs | 415 +++++++++++------- .../PluginInstaller/PluginInstallerWindow.cs | 111 +++-- .../Internal/Windows/SettingsWindow.cs | 34 ++ Dalamud/Interface/TitleScreenMenu.cs | 149 ++++++- Dalamud/Interface/UiBuilder.cs | 167 +++++-- Dalamud/Logging/Internal/TaskTracker.cs | 9 +- Dalamud/Plugin/DalamudPluginInterface.cs | 1 + Dalamud/Plugin/Internal/PluginManager.cs | 75 +++- Dalamud/Plugin/Internal/Types/LocalPlugin.cs | 229 ++++++---- .../Plugin/Internal/Types/PluginManifest.cs | 6 + Dalamud/Plugin/Internal/Types/PluginState.cs | 9 +- Dalamud/ServiceManager.cs | 113 ++++- Dalamud/Service{T}.cs | 168 +++++-- Dalamud/Utility/Util.cs | 26 ++ 55 files changed, 1809 insertions(+), 872 deletions(-) diff --git a/Dalamud.Boot/xivfixes.cpp b/Dalamud.Boot/xivfixes.cpp index 952b33802..1f11d7ed5 100644 --- a/Dalamud.Boot/xivfixes.cpp +++ b/Dalamud.Boot/xivfixes.cpp @@ -403,6 +403,96 @@ void xivfixes::redirect_openprocess(bool bApply) { } } +void xivfixes::backup_userdata_save(bool bApply) { + static const char* LogTag = "[xivfixes:backup_userdata_save]"; + static std::optional> s_hookCreateFileW; + static std::optional> s_hookCloseHandle; + static std::map> s_handles; + static std::mutex s_mtx; + + if (bApply) { + if (!g_startInfo.BootEnabledGameFixes.contains("backup_userdata_save")) { + logging::I("{} Turned off via environment variable.", LogTag); + return; + } + + s_hookCreateFileW.emplace("kernel32.dll!CreateFileW (import, backup_userdata_save)", "kernel32.dll", "CreateFileW", 0); + s_hookCloseHandle.emplace("kernel32.dll!CloseHandle (import, backup_userdata_save)", "kernel32.dll", "CloseHandle", 0); + + s_hookCreateFileW->set_detour([](LPCWSTR lpFileName, + DWORD dwDesiredAccess, + DWORD dwShareMode, + LPSECURITY_ATTRIBUTES lpSecurityAttributes, + DWORD dwCreationDisposition, + DWORD dwFlagsAndAttributes, + HANDLE hTemplateFile)->HANDLE { + if (dwDesiredAccess != GENERIC_WRITE) + return s_hookCreateFileW->call_original(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + + auto path = std::filesystem::path(lpFileName); + const auto ext = unicode::convert(path.extension().wstring(), &unicode::lower); + if (ext != ".dat" && ext != ".cfg") + return s_hookCreateFileW->call_original(lpFileName, dwDesiredAccess, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + + std::filesystem::path temporaryPath = path; + temporaryPath.replace_extension(path.extension().wstring() + L".new"); + const auto handle = s_hookCreateFileW->call_original(temporaryPath.c_str(), GENERIC_READ | GENERIC_WRITE | DELETE, dwShareMode, lpSecurityAttributes, dwCreationDisposition, dwFlagsAndAttributes, hTemplateFile); + if (handle == INVALID_HANDLE_VALUE) + return handle; + + const auto lock = std::lock_guard(s_mtx); + s_handles.try_emplace(handle, std::move(temporaryPath), std::move(path)); + + return handle; + }); + + s_hookCloseHandle->set_detour([](HANDLE handle) { + const auto lock = std::lock_guard(s_mtx); + if (const auto it = s_handles.find(handle); it != s_handles.end()) { + std::filesystem::path tempPath(std::move(it->second.first)); + std::filesystem::path finalPath(std::move(it->second.second)); + s_handles.erase(it); + + if (exists(finalPath)) { + std::filesystem::path oldPath = finalPath; + oldPath.replace_extension(finalPath.extension().wstring() + L".old"); + try { + rename(finalPath, oldPath); + } catch (const std::exception& e) { + logging::E("{0} Failed to rename {1} to {2}: {3}", + LogTag, + unicode::convert(finalPath.c_str()), + unicode::convert(oldPath.c_str()), + e.what()); + } + } + + const auto pathwstr = finalPath.wstring(); + std::vector renameInfoBuf(sizeof(FILE_RENAME_INFO) + sizeof(wchar_t) * pathwstr.size() + 2); + auto& renameInfo = *reinterpret_cast(&renameInfoBuf[0]); + renameInfo.ReplaceIfExists = true; + renameInfo.FileNameLength = static_cast(pathwstr.size() * 2); + memcpy(renameInfo.FileName, &pathwstr[0], renameInfo.FileNameLength); + if (!SetFileInformationByHandle(handle, FileRenameInfo, &renameInfoBuf[0], static_cast(renameInfoBuf.size()))) { + logging::E("{0} Failed to rename {1} to {2}: Win32 error {3}(0x{3})", + LogTag, + unicode::convert(tempPath.c_str()), + unicode::convert(finalPath.c_str()), + GetLastError()); + } + } + return s_hookCloseHandle->call_original(handle); + }); + + logging::I("{} Enable", LogTag); + } else { + if (s_hookCreateFileW) { + logging::I("{} Disable OpenProcess", LogTag); + s_hookCreateFileW.reset(); + } + } +} + void xivfixes::apply_all(bool bApply) { for (const auto& [taskName, taskFunction] : std::initializer_list> { @@ -410,6 +500,7 @@ void xivfixes::apply_all(bool bApply) { { "prevent_devicechange_crashes", &prevent_devicechange_crashes }, { "disable_game_openprocess_access_check", &disable_game_openprocess_access_check }, { "redirect_openprocess", &redirect_openprocess }, + { "backup_userdata_save", &backup_userdata_save }, } ) { try { diff --git a/Dalamud.Boot/xivfixes.h b/Dalamud.Boot/xivfixes.h index 97227cd57..556c5422f 100644 --- a/Dalamud.Boot/xivfixes.h +++ b/Dalamud.Boot/xivfixes.h @@ -5,6 +5,7 @@ namespace xivfixes { void prevent_devicechange_crashes(bool bApply); void disable_game_openprocess_access_check(bool bApply); void redirect_openprocess(bool bApply); + void backup_userdata_save(bool bApply); void apply_all(bool bApply); } diff --git a/Dalamud.Injector/EntryPoint.cs b/Dalamud.Injector/EntryPoint.cs index 46957908b..30976357d 100644 --- a/Dalamud.Injector/EntryPoint.cs +++ b/Dalamud.Injector/EntryPoint.cs @@ -315,7 +315,7 @@ namespace Dalamud.Injector startInfo.BootShowConsole = args.Contains("--console"); startInfo.BootEnableEtw = args.Contains("--etw"); startInfo.BootLogPath = GetLogPath("dalamud.boot"); - startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess" }; + startInfo.BootEnabledGameFixes = new List { "prevent_devicechange_crashes", "disable_game_openprocess_access_check", "redirect_openprocess", "backup_userdata_save" }; startInfo.BootDotnetOpenProcessHookMode = 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox1") ? 1 : 0; startInfo.BootWaitMessageBox |= args.Contains("--msgbox2") ? 2 : 0; diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index acbe1e621..ccbfc17dc 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -190,6 +190,11 @@ namespace Dalamud.Configuration.Internal /// public LogEventLevel LogLevel { get; set; } = LogEventLevel.Information; + /// + /// Gets or sets a value indicating whether to write to log files synchronously. + /// + public bool LogSynchronously { get; set; } = false; + /// /// Gets or sets a value indicating whether or not the debug log should scroll automatically. /// @@ -261,6 +266,12 @@ namespace Dalamud.Configuration.Internal /// public bool PluginSafeMode { get; set; } + /// + /// Gets or sets a value indicating the wait time between plugin unload and plugin assembly unload. + /// Uses default value that may change between versions if set to null. + /// + public int? PluginWaitBeforeFree { get; set; } + /// /// Gets or sets a list of saved styles. /// diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 747e78e25..24011b489 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -5,21 +5,11 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; -using Dalamud.Data; using Dalamud.Game; -using Dalamud.Game.ClientState; using Dalamud.Game.Gui.Internal; -using Dalamud.Game.Internal; -using Dalamud.Game.Network.Internal; -using Dalamud.Hooking.Internal; using Dalamud.Interface.Internal; -using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal; -using Dalamud.Support; -using Dalamud.Utility; using Serilog; -using Serilog.Core; -using Serilog.Events; #if DEBUG [assembly: InternalsVisibleTo("Dalamud.CorePlugin")] @@ -33,13 +23,11 @@ namespace Dalamud /// /// The main Dalamud class containing all subsystems. /// - internal sealed class Dalamud : IDisposable, IServiceType + internal sealed class Dalamud : IServiceType { #region Internals private readonly ManualResetEvent unloadSignal; - private readonly ManualResetEvent finishUnloadSignal; - private MonoMod.RuntimeDetour.Hook processMonoHook; private bool hasDisposedPlugins = false; #endregion @@ -48,22 +36,13 @@ namespace Dalamud /// Initializes a new instance of the class. /// /// DalamudStartInfo instance. - /// LoggingLevelSwitch to control Serilog level. - /// Signal signalling shutdown. /// The Dalamud configuration. /// Event used to signal the main thread to continue. - public Dalamud(DalamudStartInfo info, LoggingLevelSwitch loggingLevelSwitch, ManualResetEvent finishSignal, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) + public Dalamud(DalamudStartInfo info, DalamudConfiguration configuration, IntPtr mainThreadContinueEvent) { - this.LogLevelSwitch = loggingLevelSwitch; - this.unloadSignal = new ManualResetEvent(false); this.unloadSignal.Reset(); - this.finishUnloadSignal = finishSignal; - this.finishUnloadSignal.Reset(); - - SerilogEventSink.Instance.LogLine += SerilogOnLogLine; - ServiceManager.InitializeProvidedServicesAndClientStructs(this, info, configuration); if (!configuration.IsResumeGameAfterPluginLoad) @@ -111,11 +90,6 @@ namespace Dalamud } } - /// - /// Gets LoggingLevelSwitch for Dalamud and Plugin logs. - /// - internal LoggingLevelSwitch LogLevelSwitch { get; private set; } - /// /// Gets location of stored assets. /// @@ -138,14 +112,6 @@ namespace Dalamud this.unloadSignal.WaitOne(); } - /// - /// Wait for a queued unload to be finalized. - /// - public void WaitForUnloadFinish() - { - this.finishUnloadSignal?.WaitOne(); - } - /// /// Dispose subsystems related to plugin handling. /// @@ -169,46 +135,6 @@ namespace Dalamud Service.GetNullable()?.Dispose(); } - /// - /// Dispose Dalamud subsystems. - /// - public void Dispose() - { - try - { - if (!this.hasDisposedPlugins) - { - this.DisposePlugins(); - Thread.Sleep(100); - } - - Service.GetNullable()?.ExplicitDispose(); - Service.GetNullable()?.ExplicitDispose(); - - this.unloadSignal?.Dispose(); - - Service.GetNullable()?.Dispose(); - Service.GetNullable()?.ExplicitDispose(); - Service.GetNullable()?.Dispose(); - Service.GetNullable()?.Dispose(); - Service.GetNullable()?.Dispose(); - - var sigScanner = Service.Get(); - sigScanner.Save(); - sigScanner.Dispose(); - - SerilogEventSink.Instance.LogLine -= SerilogOnLogLine; - - this.processMonoHook?.Dispose(); - - Log.Debug("Dalamud::Dispose() OK!"); - } - catch (Exception ex) - { - Log.Error(ex, "Dalamud::Dispose() failed."); - } - } - /// /// Replace the built-in exception handler with a debug one. /// @@ -221,13 +147,5 @@ namespace Dalamud var oldFilter = NativeFunctions.SetUnhandledExceptionFilter(releaseFilter); Log.Debug("Reset ExceptionFilter, old: {0}", oldFilter); } - - private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e) - { - if (e.Exception == null) - return; - - Troubleshooting.LogException(e.Exception, e.Line); - } } } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d5e1e6656..029e8dd55 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -25,6 +25,11 @@ namespace Dalamud /// public sealed class EntryPoint { + /// + /// Log level switch for runtime log level change. + /// + public static readonly LoggingLevelSwitch LogLevelSwitch = new(LogEventLevel.Verbose); + /// /// A delegate used during initialization of the CLR from Dalamud.Boot. /// @@ -107,6 +112,49 @@ namespace Dalamud msgThread.Join(); } + /// + /// Sets up logging. + /// + /// Base directory. + /// Whether to log to console. + /// Log synchronously. + internal static void InitLogging(string baseDirectory, bool logConsole, bool logSynchronously) + { +#if DEBUG + var logPath = Path.Combine(baseDirectory, "dalamud.log"); + var oldPath = Path.Combine(baseDirectory, "dalamud.log.old"); +#else + var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log"); + var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old"); +#endif + Log.CloseAndFlush(); + + CullLogFile(logPath, oldPath, 1 * 1024 * 1024); + CullLogFile(oldPath, null, 10 * 1024 * 1024); + + var config = new LoggerConfiguration() + .WriteTo.Sink(SerilogEventSink.Instance) + .MinimumLevel.ControlledBy(LogLevelSwitch); + + if (logSynchronously) + { + config = config.WriteTo.File(logPath, fileSizeLimitBytes: null); + } + else + { + config = config.WriteTo.Async(a => a.File( + logPath, + fileSizeLimitBytes: null, + buffered: false, + flushToDiskInterval: TimeSpan.FromSeconds(1))); + } + + if (logConsole) + config = config.WriteTo.Console(); + + Log.Logger = config.CreateLogger(); + } + /// /// Initialize all Dalamud subsystems and start running on the main thread. /// @@ -115,22 +163,23 @@ namespace Dalamud private static void RunThread(DalamudStartInfo info, IntPtr mainThreadContinueEvent) { // Setup logger - var levelSwitch = InitLogging(info.WorkingDirectory, info.BootShowConsole); + InitLogging(info.WorkingDirectory!, info.BootShowConsole, true); + SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level - var configuration = DalamudConfiguration.Load(info.ConfigurationPath); + var configuration = DalamudConfiguration.Load(info.ConfigurationPath!); // Set the appropriate logging level from the configuration #if !DEBUG - levelSwitch.MinimumLevel = configuration.LogLevel; + if (!configuration.LogSynchronously) + InitLogging(info.WorkingDirectory!, info.BootShowConsole, configuration.LogSynchronously); + LogLevelSwitch.MinimumLevel = configuration.LogLevel; #endif // Log any unhandled exception. AppDomain.CurrentDomain.UnhandledException += OnUnhandledException; TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; - var finishSignal = new ManualResetEvent(false); - try { if (info.DelayInitializeMs > 0) @@ -148,12 +197,12 @@ namespace Dalamud if (!Util.IsLinux()) InitSymbolHandler(info); - var dalamud = new Dalamud(info, levelSwitch, finishSignal, configuration, mainThreadContinueEvent); + var dalamud = new Dalamud(info, configuration, mainThreadContinueEvent); Log.Information("This is Dalamud - Core: {GitHash}, CS: {CsGitHash}", Util.GetGitHash(), Util.GetGitHashClientStructs()); dalamud.WaitForUnload(); - dalamud.Dispose(); + ServiceManager.UnloadAllServices(); } catch (Exception ex) { @@ -166,11 +215,18 @@ namespace Dalamud Log.Information("Session has ended."); Log.CloseAndFlush(); - - finishSignal.Set(); + SerilogEventSink.Instance.LogLine -= SerilogOnLogLine; } } + private static void SerilogOnLogLine(object? sender, (string Line, LogEventLevel Level, DateTimeOffset TimeStamp, Exception? Exception) e) + { + if (e.Exception == null) + return; + + Troubleshooting.LogException(e.Exception, e.Line); + } + private static void InitSymbolHandler(DalamudStartInfo info) { try @@ -193,33 +249,6 @@ namespace Dalamud } } - private static LoggingLevelSwitch InitLogging(string baseDirectory, bool logConsole) - { -#if DEBUG - var logPath = Path.Combine(baseDirectory, "dalamud.log"); - var oldPath = Path.Combine(baseDirectory, "dalamud.log.old"); -#else - var logPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log"); - var oldPath = Path.Combine(baseDirectory, "..", "..", "..", "dalamud.log.old"); -#endif - - CullLogFile(logPath, oldPath, 1 * 1024 * 1024); - CullLogFile(oldPath, null, 10 * 1024 * 1024); - - var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Verbose); - var config = new LoggerConfiguration() - .WriteTo.Async(a => a.File(logPath, fileSizeLimitBytes: null, buffered: false, flushToDiskInterval: TimeSpan.FromSeconds(1))) - .WriteTo.Sink(SerilogEventSink.Instance) - .MinimumLevel.ControlledBy(levelSwitch); - - if (logConsole) - config = config.WriteTo.Console(); - - Log.Logger = config.CreateLogger(); - - return levelSwitch; - } - private static void CullLogFile(string logPath, string? oldPath, int cullingFileSize) { try diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 53636a54e..e0bd38d55 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -107,6 +107,9 @@ namespace Dalamud.Game private readonly DalamudLinkPayload openInstallerWindowLink; + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + private bool hasSeenLoadingMsg; private bool hasAutoUpdatedPlugins; @@ -118,7 +121,7 @@ namespace Dalamud.Game this.openInstallerWindowLink = chatGui.AddChatLinkHandler("Dalamud", 1001, (i, m) => { - Service.Get().OpenPluginInstaller(); + Service.GetNullable()?.OpenPluginInstaller(); }); } @@ -145,11 +148,9 @@ namespace Dalamud.Game private void OnCheckMessageHandled(XivChatType type, uint senderid, ref SeString sender, ref SeString message, ref bool isHandled) { - var configuration = Service.Get(); - var textVal = message.TextValue; - if (!configuration.DisableRmtFiltering) + if (!this.configuration.DisableRmtFiltering) { var matched = this.rmtRegex.IsMatch(textVal); if (matched) @@ -161,8 +162,8 @@ namespace Dalamud.Game } } - if (configuration.BadWords != null && - configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) + if (this.configuration.BadWords != null && + this.configuration.BadWords.Any(x => !string.IsNullOrEmpty(x) && textVal.Contains(x))) { // This seems to be in the user block list - let's not show it Log.Debug("Blocklist triggered"); @@ -174,7 +175,9 @@ namespace Dalamud.Game private void OnChatMessage(XivChatType type, uint senderId, ref SeString sender, ref SeString message, ref bool isHandled) { var startInfo = Service.Get(); - var clientState = Service.Get(); + var clientState = Service.GetNullable(); + if (clientState == null) + return; if (type == XivChatType.Notice && !this.hasSeenLoadingMsg) this.PrintWelcomeMessage(); @@ -232,17 +235,19 @@ namespace Dalamud.Game private void PrintWelcomeMessage() { - var chatGui = Service.Get(); - var configuration = Service.Get(); - var pluginManager = Service.Get(); - var dalamudInterface = Service.Get(); + var chatGui = Service.GetNullable(); + var pluginManager = Service.GetNullable(); + var dalamudInterface = Service.GetNullable(); + + if (chatGui == null || pluginManager == null || dalamudInterface == null) + return; var assemblyVersion = Assembly.GetAssembly(typeof(ChatHandlers)).GetName().Version.ToString(); chatGui.Print(string.Format(Loc.Localize("DalamudWelcome", "Dalamud vD{0} loaded."), assemblyVersion) + string.Format(Loc.Localize("PluginsWelcome", " {0} plugin(s) loaded."), pluginManager.InstalledPlugins.Count(x => x.IsLoaded))); - if (configuration.PrintPluginsWelcomeMsg) + if (this.configuration.PrintPluginsWelcomeMsg) { foreach (var plugin in pluginManager.InstalledPlugins.OrderBy(plugin => plugin.Name).Where(x => x.IsLoaded)) { @@ -250,7 +255,7 @@ namespace Dalamud.Game } } - if (string.IsNullOrEmpty(configuration.LastVersion) || !assemblyVersion.StartsWith(configuration.LastVersion)) + if (string.IsNullOrEmpty(this.configuration.LastVersion) || !assemblyVersion.StartsWith(this.configuration.LastVersion)) { chatGui.PrintChat(new XivChatEntry { @@ -258,14 +263,14 @@ namespace Dalamud.Game Type = XivChatType.Notice, }); - if (string.IsNullOrEmpty(configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor))) + if (string.IsNullOrEmpty(this.configuration.LastChangelogMajorMinor) || (!ChangelogWindow.WarrantsChangelogForMajorMinor.StartsWith(this.configuration.LastChangelogMajorMinor) && assemblyVersion.StartsWith(ChangelogWindow.WarrantsChangelogForMajorMinor))) { dalamudInterface.OpenChangelogWindow(); - configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor; + this.configuration.LastChangelogMajorMinor = ChangelogWindow.WarrantsChangelogForMajorMinor; } - configuration.LastVersion = assemblyVersion; - configuration.Save(); + this.configuration.LastVersion = assemblyVersion; + this.configuration.Save(); } this.hasSeenLoadingMsg = true; @@ -273,10 +278,12 @@ namespace Dalamud.Game private void AutoUpdatePlugins() { - var chatGui = Service.Get(); - var configuration = Service.Get(); - var pluginManager = Service.Get(); - var notifications = Service.Get(); + var chatGui = Service.GetNullable(); + var pluginManager = Service.GetNullable(); + var notifications = Service.GetNullable(); + + if (chatGui == null || pluginManager == null || notifications == null) + return; if (!pluginManager.ReposReady || pluginManager.InstalledPlugins.Count == 0 || pluginManager.AvailablePlugins.Count == 0) { @@ -286,7 +293,7 @@ namespace Dalamud.Game this.hasAutoUpdatedPlugins = true; - Task.Run(() => pluginManager.UpdatePluginsAsync(!configuration.AutoUpdatePlugins)).ContinueWith(task => + Task.Run(() => pluginManager.UpdatePluginsAsync(!this.configuration.AutoUpdatePlugins)).ContinueWith(task => { if (task.IsFaulted) { @@ -297,15 +304,13 @@ namespace Dalamud.Game var updatedPlugins = task.Result; if (updatedPlugins != null && updatedPlugins.Any()) { - if (configuration.AutoUpdatePlugins) + if (this.configuration.AutoUpdatePlugins) { PluginManager.PrintUpdatedPlugins(updatedPlugins, Loc.Localize("DalamudPluginAutoUpdate", "Auto-update:")); notifications.AddNotification(Loc.Localize("NotificationUpdatedPlugins", "{0} of your plugins were updated.").Format(updatedPlugins.Count), Loc.Localize("NotificationAutoUpdate", "Auto-Update"), NotificationType.Info); } else { - var data = Service.Get(); - chatGui.PrintChat(new XivChatEntry { Message = new SeString(new List() diff --git a/Dalamud/Game/ClientState/Buddy/BuddyList.cs b/Dalamud/Game/ClientState/Buddy/BuddyList.cs index e2f34204f..a345d27e2 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyList.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyList.cs @@ -20,12 +20,15 @@ namespace Dalamud.Game.ClientState.Buddy { private const uint InvalidObjectID = 0xE0000000; + [ServiceManager.ServiceDependency] + private readonly ClientState clientState = Service.Get(); + private readonly ClientStateAddressResolver address; [ServiceManager.ServiceConstructor] - private BuddyList(ClientState clientState) + private BuddyList() { - this.address = clientState.AddressResolver; + this.address = this.clientState.AddressResolver; Log.Verbose($"Buddy list address 0x{this.address.BuddyList.ToInt64():X}"); } @@ -145,9 +148,7 @@ namespace Dalamud.Game.ClientState.Buddy /// object containing the requested data. public BuddyMember? CreateBuddyMemberReference(IntPtr address) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (this.clientState.LocalContentId == 0) return null; if (address == IntPtr.Zero) diff --git a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs index 0029d20db..4cad665e1 100644 --- a/Dalamud/Game/ClientState/Buddy/BuddyMember.cs +++ b/Dalamud/Game/ClientState/Buddy/BuddyMember.cs @@ -11,6 +11,9 @@ namespace Dalamud.Game.ClientState.Buddy /// public unsafe class BuddyMember { + [ServiceManager.ServiceDependency] + private readonly ObjectTable objectTable = Service.Get(); + /// /// Initializes a new instance of the class. /// @@ -36,7 +39,7 @@ namespace Dalamud.Game.ClientState.Buddy /// /// This iterates the actor table, it should be used with care. /// - public GameObject? GameObject => Service.Get().SearchById(this.ObjectId); + public GameObject? GameObject => this.objectTable.SearchById(this.ObjectId); /// /// Gets the current health of this buddy. diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 5bdd9c694..bc2939f8b 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -1,22 +1,15 @@ using System; +using System.Linq; using System.Runtime.InteropServices; using Dalamud.Data; -using Dalamud.Game.ClientState.Aetherytes; -using Dalamud.Game.ClientState.Buddy; -using Dalamud.Game.ClientState.Fates; -using Dalamud.Game.ClientState.GamePad; -using Dalamud.Game.ClientState.JobGauge; -using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; -using Dalamud.Game.ClientState.Party; using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; -using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; using Serilog; @@ -33,6 +26,12 @@ namespace Dalamud.Game.ClientState private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly NetworkHandlers networkHandlers = Service.Get(); + private bool lastConditionNone = true; private bool lastFramePvP = false; @@ -42,7 +41,7 @@ namespace Dalamud.Game.ClientState internal ClientStateAddressResolver AddressResolver => this.address; [ServiceManager.ServiceConstructor] - private ClientState(Framework framework, NetworkHandlers networkHandlers, SigScanner sigScanner, DalamudStartInfo startInfo) + private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo) { this.address = new ClientStateAddressResolver(); this.address.Setup(sigScanner); @@ -53,11 +52,11 @@ namespace Dalamud.Game.ClientState Log.Verbose($"SetupTerritoryType address 0x{this.address.SetupTerritoryType.ToInt64():X}"); - this.setupTerritoryTypeHook = new Hook(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); + this.setupTerritoryTypeHook = Hook.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); - framework.Update += this.FrameworkOnOnUpdateEvent; + this.framework.Update += this.FrameworkOnOnUpdateEvent; - networkHandlers.CfPop += this.NetworkHandlersOnCfPop; + this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -106,7 +105,7 @@ namespace Dalamud.Game.ClientState /// /// Gets the local player character, if one is present. /// - public PlayerCharacter? LocalPlayer => Service.Get()[0] as PlayerCharacter; + public PlayerCharacter? LocalPlayer => Service.GetNullable()?.FirstOrDefault() as PlayerCharacter; /// /// Gets the content ID of the local character. @@ -134,10 +133,8 @@ namespace Dalamud.Game.ClientState void IDisposable.Dispose() { this.setupTerritoryTypeHook.Dispose(); - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); - Service.Get().Update -= this.FrameworkOnOnUpdateEvent; - Service.Get().CfPop -= this.NetworkHandlersOnCfPop; + this.framework.Update -= this.FrameworkOnOnUpdateEvent; + this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } [ServiceManager.CallWhenServicesReady] @@ -161,11 +158,14 @@ namespace Dalamud.Game.ClientState this.CfPop?.Invoke(this, e); } - private void FrameworkOnOnUpdateEvent(Framework framework) + private void FrameworkOnOnUpdateEvent(Framework framework1) { - var condition = Service.Get(); - var gameGui = Service.Get(); - var data = Service.Get(); + var condition = Service.GetNullable(); + var gameGui = Service.GetNullable(); + var data = Service.GetNullable(); + + if (condition == null || gameGui == null || data == null) + return; if (condition.Any() && this.lastConditionNone == true) { diff --git a/Dalamud/Game/ClientState/Fates/Fate.cs b/Dalamud/Game/ClientState/Fates/Fate.cs index c6327ed64..1e5176a9a 100644 --- a/Dalamud/Game/ClientState/Fates/Fate.cs +++ b/Dalamud/Game/ClientState/Fates/Fate.cs @@ -46,9 +46,9 @@ namespace Dalamud.Game.ClientState.Fates /// True or false. public static bool IsValid(Fate fate) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); - if (fate == null) + if (fate == null || clientState == null) return false; if (clientState.LocalContentId == 0) diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 05462c6f7..597818975 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -32,7 +32,7 @@ namespace Dalamud.Game.ClientState.GamePad { var resolver = clientState.AddressResolver; Log.Verbose($"GamepadPoll address 0x{resolver.GamepadPoll.ToInt64():X}"); - this.gamepadPoll = new Hook(resolver.GamepadPoll, this.GamepadPollDetour); + this.gamepadPoll = Hook.FromAddress(resolver.GamepadPoll, this.GamepadPollDetour); } private delegate int ControllerPoll(IntPtr controllerInput); diff --git a/Dalamud/Game/ClientState/Objects/ObjectTable.cs b/Dalamud/Game/ClientState/Objects/ObjectTable.cs index a5ae12aed..051fd36b0 100644 --- a/Dalamud/Game/ClientState/Objects/ObjectTable.cs +++ b/Dalamud/Game/ClientState/Objects/ObjectTable.cs @@ -97,9 +97,9 @@ namespace Dalamud.Game.ClientState.Objects /// object or inheritor containing the requested data. public unsafe GameObject? CreateObjectReference(IntPtr address) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); - if (clientState.LocalContentId == 0) + if (clientState == null || clientState.LocalContentId == 0) return null; if (address == IntPtr.Zero) diff --git a/Dalamud/Game/ClientState/Objects/TargetManager.cs b/Dalamud/Game/ClientState/Objects/TargetManager.cs index 54ed83546..0c97c8bcf 100644 --- a/Dalamud/Game/ClientState/Objects/TargetManager.cs +++ b/Dalamud/Game/ClientState/Objects/TargetManager.cs @@ -14,12 +14,18 @@ namespace Dalamud.Game.ClientState.Objects [ServiceManager.BlockingEarlyLoadedService] public sealed unsafe class TargetManager : IServiceType { + [ServiceManager.ServiceDependency] + private readonly ClientState clientState = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ObjectTable objectTable = Service.Get(); + private readonly ClientStateAddressResolver address; [ServiceManager.ServiceConstructor] - private TargetManager(ClientState clientState) + private TargetManager() { - this.address = clientState.AddressResolver; + this.address = this.clientState.AddressResolver; } /// @@ -32,7 +38,7 @@ namespace Dalamud.Game.ClientState.Objects /// public GameObject? Target { - get => Service.Get().CreateObjectReference((IntPtr)Struct->Target); + get => this.objectTable.CreateObjectReference((IntPtr)Struct->Target); set => this.SetTarget(value); } @@ -41,7 +47,7 @@ namespace Dalamud.Game.ClientState.Objects /// public GameObject? MouseOverTarget { - get => Service.Get().CreateObjectReference((IntPtr)Struct->MouseOverTarget); + get => this.objectTable.CreateObjectReference((IntPtr)Struct->MouseOverTarget); set => this.SetMouseOverTarget(value); } @@ -50,7 +56,7 @@ namespace Dalamud.Game.ClientState.Objects /// public GameObject? FocusTarget { - get => Service.Get().CreateObjectReference((IntPtr)Struct->FocusTarget); + get => this.objectTable.CreateObjectReference((IntPtr)Struct->FocusTarget); set => this.SetFocusTarget(value); } @@ -59,7 +65,7 @@ namespace Dalamud.Game.ClientState.Objects /// public GameObject? PreviousTarget { - get => Service.Get().CreateObjectReference((IntPtr)Struct->PreviousTarget); + get => this.objectTable.CreateObjectReference((IntPtr)Struct->PreviousTarget); set => this.SetPreviousTarget(value); } @@ -68,7 +74,7 @@ namespace Dalamud.Game.ClientState.Objects /// public GameObject? SoftTarget { - get => Service.Get().CreateObjectReference((IntPtr)Struct->SoftTarget); + get => this.objectTable.CreateObjectReference((IntPtr)Struct->SoftTarget); set => this.SetSoftTarget(value); } diff --git a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs index b574ebc13..55627e0c3 100644 --- a/Dalamud/Game/ClientState/Objects/Types/GameObject.cs +++ b/Dalamud/Game/ClientState/Objects/Types/GameObject.cs @@ -61,9 +61,9 @@ namespace Dalamud.Game.ClientState.Objects.Types /// True or false. public static bool IsValid(GameObject? actor) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); - if (actor is null) + if (actor is null || clientState == null) return false; if (clientState.LocalContentId == 0) diff --git a/Dalamud/Game/ClientState/Party/PartyList.cs b/Dalamud/Game/ClientState/Party/PartyList.cs index 8f2f96eee..40bba06d4 100644 --- a/Dalamud/Game/ClientState/Party/PartyList.cs +++ b/Dalamud/Game/ClientState/Party/PartyList.cs @@ -20,12 +20,15 @@ namespace Dalamud.Game.ClientState.Party private const int GroupLength = 8; private const int AllianceLength = 20; + [ServiceManager.ServiceDependency] + private readonly ClientState clientState = Service.Get(); + private readonly ClientStateAddressResolver address; [ServiceManager.ServiceConstructor] - private PartyList(ClientState clientState) + private PartyList() { - this.address = clientState.AddressResolver; + this.address = this.clientState.AddressResolver; Log.Verbose($"Group manager address 0x{this.address.GroupManager.ToInt64():X}"); } @@ -115,9 +118,7 @@ namespace Dalamud.Game.ClientState.Party /// The party member object containing the requested data. public PartyMember? CreatePartyMemberReference(IntPtr address) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (this.clientState.LocalContentId == 0) return null; if (address == IntPtr.Zero) @@ -146,9 +147,7 @@ namespace Dalamud.Game.ClientState.Party /// The party member object containing the requested data. public PartyMember? CreateAllianceMemberReference(IntPtr address) { - var clientState = Service.Get(); - - if (clientState.LocalContentId == 0) + if (this.clientState.LocalContentId == 0) return null; if (address == IntPtr.Zero) diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 0af2aede2..bb4b4f810 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Command [PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] - public sealed class CommandManager : IServiceType + public sealed class CommandManager : IServiceType, IDisposable { private readonly Dictionary commandMap = new(); private readonly Regex commandRegexEn = new(@"^The command (?.+) does not exist\.$", RegexOptions.Compiled); @@ -28,6 +28,9 @@ namespace Dalamud.Game.Command private readonly Regex commandRegexCn = new(@"^^(“|「)(?.+)(”|」)(出现问题:该命令不存在|出現問題:該命令不存在)。$", RegexOptions.Compiled); private readonly Regex currentLangCommandRegex; + [ServiceManager.ServiceDependency] + private readonly ChatGui chatGui = Service.Get(); + [ServiceManager.ServiceConstructor] private CommandManager(DalamudStartInfo startInfo) { @@ -40,7 +43,7 @@ namespace Dalamud.Game.Command _ => this.currentLangCommandRegex, }; - Service.Get().CheckMessageHandled += this.OnCheckMessageHandled; + this.chatGui.CheckMessageHandled += this.OnCheckMessageHandled; } /// @@ -170,5 +173,10 @@ namespace Dalamud.Game.Command } } } + + public void Dispose() + { + this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; + } } } diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 70b4fd7e8..e6a61611c 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -8,10 +8,8 @@ using System.Threading.Tasks; using Dalamud.Game.Gui; using Dalamud.Game.Gui.Toast; -using Dalamud.Game.Libc; using Dalamud.Game.Network; using Dalamud.Hooking; -using Dalamud.Interface.Internal; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Utility; @@ -32,21 +30,19 @@ namespace Dalamud.Game private readonly List runOnNextTickTaskList = new(); private readonly Stopwatch updateStopwatch = new(); - private Hook updateHook; - private Hook freeHook; - private Hook destroyHook; + private readonly Hook updateHook; + private readonly Hook destroyHook; private Thread? frameworkUpdateThread; [ServiceManager.ServiceConstructor] - private Framework(GameGui gameGui, GameNetwork gameNetwork, SigScanner sigScanner) + private Framework(SigScanner sigScanner) { this.Address = new FrameworkAddressResolver(); this.Address.Setup(sigScanner); - this.updateHook = new Hook(this.Address.TickAddress, this.HandleFrameworkUpdate); - this.freeHook = new Hook(this.Address.FreeAddress, this.HandleFrameworkFree); - this.destroyHook = new Hook(this.Address.DestroyAddress, this.HandleFrameworkDestroy); + this.updateHook = Hook.FromAddress(this.Address.TickAddress, this.HandleFrameworkUpdate); + this.destroyHook = Hook.FromAddress(this.Address.DestroyAddress, this.HandleFrameworkDestroy); } /// @@ -113,6 +109,11 @@ namespace Dalamud.Game /// public bool IsInFrameworkUpdateThread => Thread.CurrentThread == this.frameworkUpdateThread; + /// + /// Gets a value indicating whether game Framework is unloading. + /// + public bool IsFrameworkUnloading { get; internal set; } + /// /// Gets or sets a value indicating whether to dispatch update events. /// @@ -124,7 +125,8 @@ namespace Dalamud.Game /// Return type. /// Function to call. /// Task representing the pending or already completed function. - public Task RunOnFrameworkThread(Func func) => this.IsInFrameworkUpdateThread ? Task.FromResult(func()) : this.RunOnTick(func); + public Task RunOnFrameworkThread(Func func) => + this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? Task.FromResult(func()) : this.RunOnTick(func); /// /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. @@ -133,7 +135,7 @@ namespace Dalamud.Game /// Task representing the pending or already completed function. public Task RunOnFrameworkThread(Action action) { - if (this.IsInFrameworkUpdateThread) + if (this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading) { try { @@ -151,6 +153,24 @@ namespace Dalamud.Game } } + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func> func) => + this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); + + /// + /// Run given function right away if this function has been called from game's Framework.Update thread, or otherwise run on next Framework.Update call. + /// + /// Return type. + /// Function to call. + /// Task representing the pending or already completed function. + public Task RunOnFrameworkThread(Func func) => + this.IsInFrameworkUpdateThread || this.IsFrameworkUnloading ? func() : this.RunOnTick(func); + /// /// Run given function in upcoming Framework.Tick call. /// @@ -162,6 +182,16 @@ namespace Dalamud.Game /// Task representing the pending function. public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { + if (this.IsFrameworkUnloading) + { + if (delay == default && delayTicks == default) + return this.RunOnFrameworkThread(func); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + return Task.FromCanceled(cts.Token); + } + var tcs = new TaskCompletionSource(); this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() { @@ -184,6 +214,16 @@ namespace Dalamud.Game /// Task representing the pending function. public Task RunOnTick(Action action, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) { + if (this.IsFrameworkUnloading) + { + if (delay == default && delayTicks == default) + return this.RunOnFrameworkThread(action); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + return Task.FromCanceled(cts.Token); + } + var tcs = new TaskCompletionSource(); this.runOnNextTickTaskList.Add(new RunOnNextTickTaskAction() { @@ -196,22 +236,88 @@ namespace Dalamud.Game return tcs.Task; } + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func> func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + if (this.IsFrameworkUnloading) + { + if (delay == default && delayTicks == default) + return this.RunOnFrameworkThread(func); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + return Task.FromCanceled(cts.Token); + } + + var tcs = new TaskCompletionSource>(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc>() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Func = func, + }); + return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + } + + /// + /// Run given function in upcoming Framework.Tick call. + /// + /// Return type. + /// Function to call. + /// Wait for given timespan before calling this function. + /// Count given number of Framework.Tick calls before calling this function. This takes precedence over delay parameter. + /// Cancellation token which will prevent the execution of this function if wait conditions are not met. + /// Task representing the pending function. + public Task RunOnTick(Func func, TimeSpan delay = default, int delayTicks = default, CancellationToken cancellationToken = default) + { + if (this.IsFrameworkUnloading) + { + if (delay == default && delayTicks == default) + return this.RunOnFrameworkThread(func); + + var cts = new CancellationTokenSource(); + cts.Cancel(); + return Task.FromCanceled(cts.Token); + } + + var tcs = new TaskCompletionSource(); + this.runOnNextTickTaskList.Add(new RunOnNextTickTaskFunc() + { + RemainingTicks = delayTicks, + RunAfterTickCount = Environment.TickCount64 + (long)Math.Ceiling(delay.TotalMilliseconds), + CancellationToken = cancellationToken, + TaskCompletionSource = tcs, + Func = func, + }); + return tcs.Task.ContinueWith(x => x.Result, cancellationToken).Unwrap(); + } + /// /// Dispose of managed and unmanaged resources. /// void IDisposable.Dispose() { - Service.GetNullable()?.ExplicitDispose(); - Service.GetNullable()?.ExplicitDispose(); + this.RunOnFrameworkThread(() => + { + // ReSharper disable once AccessToDisposedClosure + this.updateHook.Disable(); - this.updateHook?.Disable(); - this.freeHook?.Disable(); - this.destroyHook?.Disable(); - Thread.Sleep(500); + // ReSharper disable once AccessToDisposedClosure + this.destroyHook.Disable(); + }).Wait(); - this.updateHook?.Dispose(); - this.freeHook?.Dispose(); - this.destroyHook?.Dispose(); + this.updateHook.Dispose(); + this.destroyHook.Dispose(); this.updateStopwatch.Reset(); statsStopwatch.Reset(); @@ -221,7 +327,6 @@ namespace Dalamud.Game private void ContinueConstruction() { this.updateHook.Enable(); - this.freeHook.Enable(); this.destroyHook.Enable(); } @@ -314,41 +419,21 @@ namespace Dalamud.Game } original: - return this.updateHook.Original(framework); + return this.updateHook.OriginalDisposeSafe(framework); } private bool HandleFrameworkDestroy(IntPtr framework) { - if (this.DispatchUpdateEvents) - { - Log.Information("Framework::Destroy!"); - - var dalamud = Service.Get(); - dalamud.DisposePlugins(); - - Log.Information("Framework::Destroy OK!"); - } - + this.IsFrameworkUnloading = true; this.DispatchUpdateEvents = false; - return this.destroyHook.Original(framework); - } + Log.Information("Framework::Destroy!"); + Service.Get().Unload(); + this.runOnNextTickTaskList.RemoveAll(x => x.Run()); + ServiceManager.UnloadAllServices(); + Log.Information("Framework::Destroy OK!"); - private IntPtr HandleFrameworkFree() - { - Log.Information("Framework::Free!"); - - // Store the pointer to the original trampoline location - var originalPtr = Marshal.GetFunctionPointerForDelegate(this.freeHook.Original); - - var dalamud = Service.Get(); - dalamud.Unload(); - dalamud.WaitForUnloadFinish(); - - Log.Information("Framework::Free OK!"); - - // Return the original trampoline location to cleanly exit - return originalPtr; + return this.destroyHook.OriginalDisposeSafe(framework); } private abstract class RunOnNextTickTaskBase diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 96e7b9eaf..48ff123c6 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -33,6 +33,12 @@ namespace Dalamud.Game.Gui private readonly Hook populateItemLinkHook; private readonly Hook interactableLinkClickedHook; + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly LibcFunction libcFunction = Service.Get(); + private IntPtr baseAddress = IntPtr.Zero; [ServiceManager.ServiceConstructor] @@ -41,9 +47,9 @@ namespace Dalamud.Game.Gui this.address = new ChatGuiAddressResolver(); this.address.Setup(sigScanner); - this.printMessageHook = new Hook(this.address.PrintMessage, this.HandlePrintMessageDetour); - this.populateItemLinkHook = new Hook(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); - this.interactableLinkClickedHook = new Hook(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); + this.printMessageHook = Hook.FromAddress(this.address.PrintMessage, this.HandlePrintMessageDetour); + this.populateItemLinkHook = Hook.FromAddress(this.address.PopulateItemLinkObject, this.HandlePopulateItemLinkDetour); + this.interactableLinkClickedHook = Hook.FromAddress(this.address.InteractableLinkClicked, this.InteractableLinkClickedDetour); } /// @@ -150,13 +156,11 @@ namespace Dalamud.Game.Gui /// A message to send. public void Print(string message) { - var configuration = Service.Get(); - // Log.Verbose("[CHATGUI PRINT REGULAR]{0}", message); this.PrintChat(new XivChatEntry { Message = message, - Type = configuration.GeneralChatType, + Type = this.configuration.GeneralChatType, }); } @@ -167,13 +171,11 @@ namespace Dalamud.Game.Gui /// A message to send. public void Print(SeString message) { - var configuration = Service.Get(); - // Log.Verbose("[CHATGUI PRINT SESTRING]{0}", message.TextValue); this.PrintChat(new XivChatEntry { Message = message, - Type = configuration.GeneralChatType, + Type = this.configuration.GeneralChatType, }); } @@ -222,10 +224,10 @@ namespace Dalamud.Game.Gui } var senderRaw = (chat.Name ?? string.Empty).Encode(); - using var senderOwned = Service.Get().NewString(senderRaw); + using var senderOwned = this.libcFunction.NewString(senderRaw); var messageRaw = (chat.Message ?? string.Empty).Encode(); - using var messageOwned = Service.Get().NewString(messageRaw); + using var messageOwned = this.libcFunction.NewString(messageRaw); this.HandlePrintMessageDetour(this.baseAddress, chat.Type, senderOwned.Address, messageOwned.Address, chat.SenderId, chat.Parameters); } @@ -364,7 +366,7 @@ namespace Dalamud.Game.Gui if (!Util.FastByteArrayCompare(originalMessageData, message.RawData)) { - allocatedString = Service.Get().NewString(message.RawData); + allocatedString = this.libcFunction.NewString(message.RawData); Log.Debug($"HandlePrintMessageDetour String modified: {originalMessageData}({messagePtr}) -> {message}({allocatedString.Address})"); messagePtr = allocatedString.Address; } @@ -379,7 +381,7 @@ namespace Dalamud.Game.Gui if (!Util.FastByteArrayCompare(originalSenderData, sender.RawData)) { - allocatedStringSender = Service.Get().NewString(sender.RawData); + allocatedStringSender = this.libcFunction.NewString(sender.RawData); Log.Debug( $"HandlePrintMessageDetour Sender modified: {originalSenderData}({senderPtr}) -> {sender}({allocatedStringSender.Address})"); senderPtr = allocatedStringSender.Address; diff --git a/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs index 2e14e6402..0566b8107 100644 --- a/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenus/ContextMenu.cs @@ -58,11 +58,11 @@ namespace Dalamud.Game.Gui.ContextMenus { this.openSubContextMenu = Marshal.GetDelegateForFunctionPointer(this.Address.OpenSubContextMenuPtr); - this.contextMenuOpeningHook = new Hook(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour); - this.contextMenuOpenedHook = new Hook(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour); - this.contextMenuItemSelectedHook = new Hook(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour); - this.subContextMenuOpeningHook = new Hook(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour); - this.subContextMenuOpenedHook = new Hook(this.Address.SubContextMenuOpenedPtr, this.SubContextMenuOpenedDetour); + this.contextMenuOpeningHook = Hook.FromAddress(this.Address.ContextMenuOpeningPtr, this.ContextMenuOpeningDetour); + this.contextMenuOpenedHook = Hook.FromAddress(this.Address.ContextMenuOpenedPtr, this.ContextMenuOpenedDetour); + this.contextMenuItemSelectedHook = Hook.FromAddress(this.Address.ContextMenuItemSelectedPtr, this.ContextMenuItemSelectedDetour); + this.subContextMenuOpeningHook = Hook.FromAddress(this.Address.SubContextMenuOpeningPtr, this.SubContextMenuOpeningDetour); + this.subContextMenuOpenedHook = Hook.FromAddress(this.Address.SubContextMenuOpenedPtr, this.SubContextMenuOpenedDetour); } } diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 6ff9dc83f..e70bad070 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,17 +22,26 @@ namespace Dalamud.Game.Gui.Dtr { private const uint BaseNodeId = 1000; + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + private List entries = new(); private uint runningNodeIds = BaseNodeId; [ServiceManager.ServiceConstructor] - private DtrBar(DalamudConfiguration configuration, Framework framework) + private DtrBar() { - framework.Update += this.Update; + this.framework.Update += this.Update; - configuration.DtrOrder ??= new List(); - configuration.DtrIgnore ??= new List(); - configuration.Save(); + this.configuration.DtrOrder ??= new List(); + this.configuration.DtrIgnore ??= new List(); + this.configuration.Save(); } /// @@ -48,14 +57,13 @@ namespace Dalamud.Game.Gui.Dtr if (this.entries.Any(x => x.Title == title)) throw new ArgumentException("An entry with the same title already exists."); - var configuration = Service.Get(); var node = this.MakeNode(++this.runningNodeIds); var entry = new DtrBarEntry(title, node); entry.Text = text; // Add the entry to the end of the order list, if it's not there already. - if (!configuration.DtrOrder!.Contains(title)) - configuration.DtrOrder!.Add(title); + if (!this.configuration.DtrOrder!.Contains(title)) + this.configuration.DtrOrder!.Add(title); this.entries.Add(entry); this.ApplySort(); @@ -69,7 +77,7 @@ namespace Dalamud.Game.Gui.Dtr this.RemoveNode(entry.TextNode); this.entries.Clear(); - Service.Get().Update -= this.Update; + this.framework.Update -= this.Update; } /// @@ -112,12 +120,11 @@ namespace Dalamud.Game.Gui.Dtr /// internal void ApplySort() { - var configuration = Service.Get(); - // Sort the current entry list, based on the order in the configuration. - var positions = configuration.DtrOrder! - .Select(entry => (entry, index: configuration.DtrOrder!.IndexOf(entry))) - .ToDictionary(x => x.entry, x => x.index); + var positions = this.configuration + .DtrOrder! + .Select(entry => (entry, index: this.configuration.DtrOrder!.IndexOf(entry))) + .ToDictionary(x => x.entry, x => x.index); this.entries.Sort((x, y) => { @@ -127,13 +134,13 @@ namespace Dalamud.Game.Gui.Dtr }); } - private static AtkUnitBase* GetDtr() => (AtkUnitBase*)Service.Get().GetAddonByName("_DTR", 1).ToPointer(); + private AtkUnitBase* GetDtr() => (AtkUnitBase*)this.gameGui.GetAddonByName("_DTR", 1).ToPointer(); private void Update(Framework unused) { this.HandleRemovedNodes(); - var dtr = GetDtr(); + var dtr = this.GetDtr(); if (dtr == null) return; // The collision node on the DTR element is always the width of its content @@ -147,16 +154,16 @@ namespace Dalamud.Game.Gui.Dtr var collisionNode = dtr->UldManager.NodeList[1]; if (collisionNode == null) return; - var configuration = Service.Get(); - // If we are drawing backwards, we should start from the right side of the collision node. That is, // collisionNode->X + collisionNode->Width. - var runningXPos = configuration.DtrSwapDirection ? collisionNode->X + collisionNode->Width : collisionNode->X; + var runningXPos = this.configuration.DtrSwapDirection + ? collisionNode->X + collisionNode->Width + : collisionNode->X; for (var i = 0; i < this.entries.Count; i++) { var data = this.entries[i]; - var isHide = configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; + var isHide = this.configuration.DtrIgnore!.Any(x => x == data.Title) || !data.Shown; if (data.Dirty && data.Added && data.Text != null && data.TextNode != null) { @@ -185,9 +192,9 @@ namespace Dalamud.Game.Gui.Dtr if (!isHide) { - var elementWidth = data.TextNode->AtkResNode.Width + configuration.DtrSpacing; + var elementWidth = data.TextNode->AtkResNode.Width + this.configuration.DtrSpacing; - if (configuration.DtrSwapDirection) + if (this.configuration.DtrSwapDirection) { data.TextNode->AtkResNode.SetPositionFloat(runningXPos, 2); runningXPos += elementWidth; @@ -209,7 +216,7 @@ namespace Dalamud.Game.Gui.Dtr /// True if there are nodes with an ID > 1000. private bool CheckForDalamudNodes() { - var dtr = GetDtr(); + var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null) return false; for (var i = 0; i < dtr->UldManager.NodeListCount; i++) @@ -233,7 +240,7 @@ namespace Dalamud.Game.Gui.Dtr private bool AddNode(AtkTextNode* node) { - var dtr = GetDtr(); + var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; var lastChild = dtr->RootNode->ChildNode; @@ -253,7 +260,7 @@ namespace Dalamud.Game.Gui.Dtr private bool RemoveNode(AtkTextNode* node) { - var dtr = GetDtr(); + var dtr = this.GetDtr(); if (dtr == null || dtr->RootNode == null || dtr->UldManager.NodeList == null || node == null) return false; var tmpPrevNode = node->AtkResNode.PrevSiblingNode; diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 736ad85db..9c8d7a87c 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -36,7 +36,7 @@ namespace Dalamud.Game.Gui.FlyText this.Address.Setup(sigScanner); this.addFlyTextNative = Marshal.GetDelegateForFunctionPointer(this.Address.AddFlyText); - this.createFlyTextHook = new Hook(this.Address.CreateFlyText, this.CreateFlyTextDetour); + this.createFlyTextHook = Hook.FromAddress(this.Address.CreateFlyText, this.CreateFlyTextDetour); } /// diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index b9f6f8e3b..74f07f85d 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -60,23 +60,23 @@ namespace Dalamud.Game.Gui Log.Verbose($"HandleItemOut address 0x{this.address.HandleItemOut.ToInt64():X}"); Log.Verbose($"HandleImm address 0x{this.address.HandleImm.ToInt64():X}"); - this.setGlobalBgmHook = new Hook(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour); + this.setGlobalBgmHook = Hook.FromAddress(this.address.SetGlobalBgm, this.HandleSetGlobalBgmDetour); - this.handleItemHoverHook = new Hook(this.address.HandleItemHover, this.HandleItemHoverDetour); - this.handleItemOutHook = new Hook(this.address.HandleItemOut, this.HandleItemOutDetour); + this.handleItemHoverHook = Hook.FromAddress(this.address.HandleItemHover, this.HandleItemHoverDetour); + this.handleItemOutHook = Hook.FromAddress(this.address.HandleItemOut, this.HandleItemOutDetour); - this.handleActionHoverHook = new Hook(this.address.HandleActionHover, this.HandleActionHoverDetour); - this.handleActionOutHook = new Hook(this.address.HandleActionOut, this.HandleActionOutDetour); + this.handleActionHoverHook = Hook.FromAddress(this.address.HandleActionHover, this.HandleActionHoverDetour); + this.handleActionOutHook = Hook.FromAddress(this.address.HandleActionOut, this.HandleActionOutDetour); - this.handleImmHook = new Hook(this.address.HandleImm, this.HandleImmDetour); + this.handleImmHook = Hook.FromAddress(this.address.HandleImm, this.HandleImmDetour); this.getMatrixSingleton = Marshal.GetDelegateForFunctionPointer(this.address.GetMatrixSingleton); this.screenToWorldNative = Marshal.GetDelegateForFunctionPointer(this.address.ScreenToWorld); - this.toggleUiHideHook = new Hook(this.address.ToggleUiHide, this.ToggleUiHideDetour); + this.toggleUiHideHook = Hook.FromAddress(this.address.ToggleUiHide, this.ToggleUiHideDetour); - this.utf8StringFromSequenceHook = new Hook(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); + this.utf8StringFromSequenceHook = Hook.FromAddress(this.address.Utf8StringFromSequence, this.Utf8StringFromSequenceDetour); } // Marshaled delegates @@ -436,12 +436,6 @@ namespace Dalamud.Game.Gui /// void IDisposable.Dispose() { - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); - Service.Get().ExplicitDispose(); this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); this.handleItemOutHook.Dispose(); diff --git a/Dalamud/Game/Gui/Internal/DalamudIME.cs b/Dalamud/Game/Gui/Internal/DalamudIME.cs index 811e25997..ab36c4211 100644 --- a/Dalamud/Game/Gui/Internal/DalamudIME.cs +++ b/Dalamud/Game/Gui/Internal/DalamudIME.cs @@ -266,9 +266,9 @@ namespace Dalamud.Game.Gui.Internal private void ToggleWindow(bool visible) { if (visible) - Service.Get().OpenImeWindow(); + Service.GetNullable()?.OpenImeWindow(); else - Service.Get().CloseImeWindow(); + Service.GetNullable()?.CloseImeWindow(); } } } diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 406051a99..2e9fa8c0e 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -35,7 +35,7 @@ namespace Dalamud.Game.Gui.PartyFinder this.memory = Marshal.AllocHGlobal(PartyFinderPacket.PacketSize); - this.receiveListingHook = new Hook(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); + this.receiveListingHook = Hook.FromAddress(this.address.ReceiveListing, new ReceiveListingDelegate(this.HandleReceiveListingDetour)); } /// diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 8602be735..31f7711ba 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -39,9 +39,9 @@ namespace Dalamud.Game.Gui.Toast this.address = new ToastGuiAddressResolver(); this.address.Setup(sigScanner); - this.showNormalToastHook = new Hook(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); - this.showQuestToastHook = new Hook(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); - this.showErrorToastHook = new Hook(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); + this.showNormalToastHook = Hook.FromAddress(this.address.ShowNormalToast, new ShowNormalToastDelegate(this.HandleNormalToastDetour)); + this.showQuestToastHook = Hook.FromAddress(this.address.ShowQuestToast, new ShowQuestToastDelegate(this.HandleQuestToastDetour)); + this.showErrorToastHook = Hook.FromAddress(this.address.ShowErrorToast, new ShowErrorToastDelegate(this.HandleErrorToastDetour)); } #region Event delegates diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 575801b33..cd8ca156c 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -9,7 +9,6 @@ using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Hooking; -using Dalamud.Interface; using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using FFXIVClientStructs.FFXIV.Component.GUI; @@ -35,15 +34,21 @@ namespace Dalamud.Game.Internal private readonly Hook hookAtkUnitBaseReceiveGlobalEvent; + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ContextMenu contextMenu = Service.Get(); + private readonly string locDalamudPlugins; private readonly string locDalamudSettings; [ServiceManager.ServiceConstructor] - private DalamudAtkTweaks(SigScanner sigScanner, ContextMenu contextMenu) + private DalamudAtkTweaks(SigScanner sigScanner) { var openSystemMenuAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 32 C0 4C 8B AC 24 ?? ?? ?? ?? 48 8B 8D ?? ?? ?? ??"); - this.hookAgentHudOpenSystemMenu = new Hook(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour); + this.hookAgentHudOpenSystemMenu = Hook.FromAddress(openSystemMenuAddress, this.AgentHudOpenSystemMenuDetour); var atkValueChangeTypeAddress = sigScanner.ScanText("E8 ?? ?? ?? ?? 45 84 F6 48 8D 4C 24 ??"); this.atkValueChangeType = Marshal.GetDelegateForFunctionPointer(atkValueChangeTypeAddress); @@ -52,15 +57,15 @@ namespace Dalamud.Game.Internal this.atkValueSetString = Marshal.GetDelegateForFunctionPointer(atkValueSetStringAddress); var uiModuleRequestMainCommandAddress = sigScanner.ScanText("40 53 56 48 81 EC ?? ?? ?? ?? 48 8B 05 ?? ?? ?? ?? 48 33 C4 48 89 84 24 ?? ?? ?? ?? 48 8B 01 8B DA 48 8B F1 FF 90 ?? ?? ?? ??"); - this.hookUiModuleRequestMainCommand = new Hook(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour); + this.hookUiModuleRequestMainCommand = Hook.FromAddress(uiModuleRequestMainCommandAddress, this.UiModuleRequestMainCommandDetour); var atkUnitBaseReceiveGlobalEventAddress = sigScanner.ScanText("48 89 5C 24 ?? 48 89 7C 24 ?? 55 41 56 41 57 48 8B EC 48 83 EC 50 44 0F B7 F2 "); - this.hookAtkUnitBaseReceiveGlobalEvent = new Hook(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour); + this.hookAtkUnitBaseReceiveGlobalEvent = Hook.FromAddress(atkUnitBaseReceiveGlobalEventAddress, this.AtkUnitBaseReceiveGlobalEventDetour); this.locDalamudPlugins = Loc.Localize("SystemMenuPlugins", "Dalamud Plugins"); this.locDalamudSettings = Loc.Localize("SystemMenuSettings", "Dalamud Settings"); - contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; + this.contextMenu.ContextMenuOpened += this.ContextMenuOnContextMenuOpened; } private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); @@ -83,11 +88,13 @@ namespace Dalamud.Game.Internal private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { - var systemText = Service.Get().GetExcelSheet()!.GetRow(1059)!.Text.RawString; // "System" - var configuration = Service.Get(); - var interfaceManager = Service.Get(); + var systemText = Service.GetNullable()?.GetExcelSheet()?.GetRow(1059)?.Text?.RawString; // "System" + var interfaceManager = Service.GetNullable(); - if (args.Title == systemText && configuration.DoButtonsSystemMenu && interfaceManager.IsDispatchingEvents) + if (systemText == null || interfaceManager == null) + return; + + if (args.Title == systemText && this.configuration.DoButtonsSystemMenu && interfaceManager.IsDispatchingEvents) { var dalamudInterface = Service.Get(); @@ -109,7 +116,7 @@ namespace Dalamud.Game.Internal // "SendHotkey" // 3 == Close - if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && Service.Get().IsFocusManagementEnabled) + if (cmd == 12 && WindowSystem.HasAnyWindowSystemFocus && *arg == 3 && this.configuration.IsFocusManagementEnabled) { Log.Verbose($"Cancelling global event SendHotkey command due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); return IntPtr.Zero; @@ -120,14 +127,18 @@ namespace Dalamud.Game.Internal private void AgentHudOpenSystemMenuDetour(void* thisPtr, AtkValue* atkValueArgs, uint menuSize) { - if (WindowSystem.HasAnyWindowSystemFocus && Service.Get().IsFocusManagementEnabled) + if (WindowSystem.HasAnyWindowSystemFocus && this.configuration.IsFocusManagementEnabled) { Log.Verbose($"Cancelling OpenSystemMenu due to WindowSystem {WindowSystem.FocusedWindowSystemNamespace}"); return; } - var configuration = Service.Get(); - var interfaceManager = Service.Get(); + var interfaceManager = Service.GetNullable(); + if (interfaceManager == null) + { + this.hookAgentHudOpenSystemMenu.Original(thisPtr, atkValueArgs, menuSize); + return; + } if (!configuration.DoButtonsSystemMenu || !interfaceManager.IsDispatchingEvents) { @@ -207,15 +218,15 @@ namespace Dalamud.Game.Internal private void UiModuleRequestMainCommandDetour(void* thisPtr, int commandId) { - var dalamudInterface = Service.Get(); + var dalamudInterface = Service.GetNullable(); switch (commandId) { case 69420: - dalamudInterface.TogglePluginInstallerWindow(); + dalamudInterface?.TogglePluginInstallerWindow(); break; case 69421: - dalamudInterface.ToggleSettingsWindow(); + dalamudInterface?.ToggleSettingsWindow(); break; default: this.hookUiModuleRequestMainCommand.Original(thisPtr, commandId); @@ -259,7 +270,7 @@ namespace Dalamud.Game.Internal this.hookUiModuleRequestMainCommand.Dispose(); this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); - Service.Get().ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; + this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; } this.disposed = true; diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 2f2903a8b..0b1d5375d 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -34,8 +34,8 @@ namespace Dalamud.Game.Network Log.Verbose($"ProcessZonePacketDown address 0x{this.address.ProcessZonePacketDown.ToInt64():X}"); Log.Verbose($"ProcessZonePacketUp address 0x{this.address.ProcessZonePacketUp.ToInt64():X}"); - this.processZonePacketDownHook = new Hook(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); - this.processZonePacketUpHook = new Hook(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); + this.processZonePacketDownHook = Hook.FromAddress(this.address.ProcessZonePacketDown, this.ProcessZonePacketDownDetour); + this.processZonePacketUpHook = Hook.FromAddress(this.address.ProcessZonePacketUp, this.ProcessZonePacketUpDetour); } /// diff --git a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs index b78ec1ef0..55f005a5b 100644 --- a/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs +++ b/Dalamud/Game/Network/Internal/MarketBoardUploaders/Universalis/UniversalisMarketBoardUploader.cs @@ -32,7 +32,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis /// public async Task Upload(MarketBoardItemRequest request) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); + if (clientState == null) + return; Log.Verbose("Starting Universalis upload."); var uploader = clientState.LocalContentId; @@ -118,7 +120,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis /// public async Task UploadTax(MarketTaxRates taxRates) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); + if (clientState == null) + return; // ==================================================================================== @@ -157,7 +161,9 @@ namespace Dalamud.Game.Network.Internal.MarketBoardUploaders.Universalis /// public async Task UploadPurchase(MarketBoardPurchaseHandler purchaseHandler) { - var clientState = Service.Get(); + var clientState = Service.GetNullable(); + if (clientState == null) + return; var itemId = purchaseHandler.CatalogId; var worldId = clientState.LocalPlayer?.CurrentWorld.Id ?? 0; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index e8380459f..cd774826f 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -28,6 +28,9 @@ namespace Dalamud.Game.Network.Internal private readonly IMarketBoardUploader uploader; + [ServiceManager.ServiceDependency] + private readonly DalamudConfiguration configuration = Service.Get(); + private MarketBoardPurchaseHandler marketBoardPurchaseHandler; [ServiceManager.ServiceConstructor] @@ -47,14 +50,12 @@ namespace Dalamud.Game.Network.Internal { var dataManager = Service.GetNullable(); - if (dataManager?.IsDataReady == false) + if (dataManager?.IsDataReady != true) return; - var configuration = Service.Get(); - if (direction == NetworkMessageDirection.ZoneUp) { - if (configuration.IsMbCollect) + if (this.configuration.IsMbCollect) { if (opCode == dataManager.ClientOpCodes["MarketBoardPurchaseHandler"]) { @@ -72,7 +73,7 @@ namespace Dalamud.Game.Network.Internal return; } - if (configuration.IsMbCollect) + if (this.configuration.IsMbCollect) { if (opCode == dataManager.ServerOpCodes["MarketBoardItemRequestStart"]) { @@ -236,8 +237,9 @@ namespace Dalamud.Game.Network.Internal private unsafe void HandleCfPop(IntPtr dataPtr) { - var dataManager = Service.Get(); - var configuration = Service.Get(); + var dataManager = Service.GetNullable(); + if (dataManager == null) + return; using var stream = new UnmanagedMemoryStream((byte*)dataPtr.ToPointer(), 64); using var reader = new BinaryReader(stream); @@ -266,7 +268,7 @@ namespace Dalamud.Game.Network.Internal } // Flash window - if (configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) + if (this.configuration.DutyFinderTaskbarFlash && !NativeFunctions.ApplicationIsActivated()) { var flashInfo = new NativeFunctions.FlashWindowInfo { @@ -281,9 +283,9 @@ namespace Dalamud.Game.Network.Internal Task.Run(() => { - if (configuration.DutyFinderChatMessage) + if (this.configuration.DutyFinderChatMessage) { - Service.Get().Print($"Duty pop: {cfcName}"); + Service.GetNullable()?.Print($"Duty pop: {cfcName}"); } this.CfPop?.Invoke(this, cfCondition); diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index 26b620433..ed90b90de 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -18,7 +18,7 @@ namespace Dalamud.Game.Network.Internal [ServiceManager.ServiceConstructor] private WinSockHandlers() { - this.ws2SocketHook = Hook.FromImport(Process.GetCurrentProcess().MainModule, "ws2_32.dll", "socket", 23, this.OnSocket); + this.ws2SocketHook = Hook.FromImport(null, "ws2_32.dll", "socket", 23, this.OnSocket); this.ws2SocketHook?.Enable(); } diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index bab5c4e6d..0ccc445f6 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -359,6 +359,7 @@ namespace Dalamud.Game /// public void Dispose() { + this.Save(); Marshal.FreeHGlobal(this.moduleCopyPtr); } @@ -370,7 +371,14 @@ namespace Dalamud.Game if (this.cacheFile == null) return; - File.WriteAllText(this.cacheFile.FullName, JsonConvert.SerializeObject(this.textCache)); + try + { + File.WriteAllText(this.cacheFile.FullName, JsonConvert.SerializeObject(this.textCache)); + } + catch (Exception e) + { + Log.Warning(e, "Failed to save cache to {0}", this.cacheFile); + } } /// diff --git a/Dalamud/Hooking/Hook.cs b/Dalamud/Hooking/Hook.cs index 39821f847..cfe388ef6 100644 --- a/Dalamud/Hooking/Hook.cs +++ b/Dalamud/Hooking/Hook.cs @@ -88,6 +88,22 @@ namespace Dalamud.Hooking /// Hook is already disposed. public virtual T Original => this.compatHookImpl != null ? this.compatHookImpl!.Original : throw new NotImplementedException(); + /// + /// Gets a delegate function that can be used to call the actual function as if function is not hooked yet. + /// This can be called even after Dispose. + /// + public T OriginalDisposeSafe + { + get + { + if (this.compatHookImpl != null) + return this.compatHookImpl!.OriginalDisposeSafe; + if (this.IsDisposed) + return Marshal.GetDelegateForFunctionPointer(this.address); + return this.Original; + } + } + /// /// Gets a value indicating whether or not the hook is enabled. /// @@ -115,14 +131,17 @@ namespace Dalamud.Hooking /// /// Creates a hook by rewriting import table address. /// - /// Module to check for. + /// Module to check for. Current process' main module if null. /// Name of the DLL, including the extension. /// Decorated name of the function. /// Hint or ordinal. 0 to unspecify. /// Callback function. Delegate must have a same original function prototype. /// The hook with the supplied parameters. - public static unsafe Hook FromImport(ProcessModule module, string moduleName, string functionName, uint hintOrOrdinal, T detour) + public static unsafe Hook FromImport(ProcessModule? module, string moduleName, string functionName, uint hintOrOrdinal, T detour) { + module ??= Process.GetCurrentProcess().MainModule; + if (module == null) + throw new InvalidOperationException("Current module is null?"); var pDos = (PeHeader.IMAGE_DOS_HEADER*)module.BaseAddress; var pNt = (PeHeader.IMAGE_FILE_HEADER*)(module.BaseAddress + (int)pDos->e_lfanew + 4); var isPe64 = pNt->SizeOfOptionalHeader == Marshal.SizeOf(); diff --git a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs index 9d1c289d8..d34072f52 100644 --- a/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs +++ b/Dalamud/Hooking/Internal/FunctionPointerVariableHook.cs @@ -27,24 +27,27 @@ namespace Dalamud.Hooking.Internal internal FunctionPointerVariableHook(IntPtr address, T detour, Assembly callingAssembly) : base(address) { - var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); - if (!hasOtherHooks) + lock (HookManager.HookEnableSyncRoot) { - MemoryHelper.ReadRaw(this.Address, 0x32, out var original); - HookManager.Originals[this.Address] = original; + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) + indexList = HookManager.MultiHookTracker[this.Address] = new(); + + this.pfnOriginal = Marshal.ReadIntPtr(this.Address); + this.originalDelegate = Marshal.GetDelegateForFunctionPointer(this.pfnOriginal); + this.detourDelegate = detour; + + // Add afterwards, so the hookIdent starts at 0. + indexList.Add(this); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } - - if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) - indexList = HookManager.MultiHookTracker[this.Address] = new(); - - this.pfnOriginal = Marshal.ReadIntPtr(this.Address); - this.originalDelegate = Marshal.GetDelegateForFunctionPointer(this.pfnOriginal); - this.detourDelegate = detour; - - // Add afterwards, so the hookIdent starts at 0. - indexList.Add(this); - - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } /// @@ -91,11 +94,15 @@ namespace Dalamud.Hooking.Internal if (!this.enabled) { - if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) - throw new Win32Exception(Marshal.GetLastWin32Error()); + lock (HookManager.HookEnableSyncRoot) + { + if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), + MemoryProtection.ExecuteReadWrite, out var oldProtect)) + throw new Win32Exception(Marshal.GetLastWin32Error()); - Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate)); - NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + Marshal.WriteIntPtr(this.Address, Marshal.GetFunctionPointerForDelegate(this.detourDelegate)); + NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + } } } @@ -106,11 +113,15 @@ namespace Dalamud.Hooking.Internal if (this.enabled) { - if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), MemoryProtection.ExecuteReadWrite, out var oldProtect)) - throw new Win32Exception(Marshal.GetLastWin32Error()); + lock (HookManager.HookEnableSyncRoot) + { + if (!NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), + MemoryProtection.ExecuteReadWrite, out var oldProtect)) + throw new Win32Exception(Marshal.GetLastWin32Error()); - Marshal.WriteIntPtr(this.Address, this.pfnOriginal); - NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + Marshal.WriteIntPtr(this.Address, this.pfnOriginal); + NativeFunctions.VirtualProtect(this.Address, (UIntPtr)Marshal.SizeOf(), oldProtect, out _); + } } } } diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 0a8ee331a..b87ab1796 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -23,6 +23,11 @@ namespace Dalamud.Hooking.Internal { } + /// + /// Gets sync root object for hook enabling/disabling. + /// + internal static object HookEnableSyncRoot { get; } = new(); + /// /// Gets a static list of tracked and registered hooks. /// diff --git a/Dalamud/Hooking/Internal/MinHookHook.cs b/Dalamud/Hooking/Internal/MinHookHook.cs index 563b137a1..ce4478ba8 100644 --- a/Dalamud/Hooking/Internal/MinHookHook.cs +++ b/Dalamud/Hooking/Internal/MinHookHook.cs @@ -22,24 +22,27 @@ namespace Dalamud.Hooking.Internal internal MinHookHook(IntPtr address, T detour, Assembly callingAssembly) : base(address) { - var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); - if (!hasOtherHooks) + lock (HookManager.HookEnableSyncRoot) { - MemoryHelper.ReadRaw(this.Address, 0x32, out var original); - HookManager.Originals[this.Address] = original; + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) + indexList = HookManager.MultiHookTracker[this.Address] = new(); + + var index = (ulong)indexList.Count; + + this.minHookImpl = new MinSharp.Hook(this.Address, detour, index); + + // Add afterwards, so the hookIdent starts at 0. + indexList.Add(this); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } - - if (!HookManager.MultiHookTracker.TryGetValue(this.Address, out var indexList)) - indexList = HookManager.MultiHookTracker[this.Address] = new(); - - var index = (ulong)indexList.Count; - - this.minHookImpl = new MinSharp.Hook(this.Address, detour, index); - - // Add afterwards, so the hookIdent starts at 0. - indexList.Add(this); - - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } /// @@ -71,10 +74,13 @@ namespace Dalamud.Hooking.Internal if (this.IsDisposed) return; - this.minHookImpl.Dispose(); + lock (HookManager.HookEnableSyncRoot) + { + this.minHookImpl.Dispose(); - var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); - HookManager.MultiHookTracker[this.Address][index] = null; + var index = HookManager.MultiHookTracker[this.Address].IndexOf(this); + HookManager.MultiHookTracker[this.Address][index] = null; + } base.Dispose(); } @@ -86,7 +92,10 @@ namespace Dalamud.Hooking.Internal if (!this.minHookImpl.Enabled) { - this.minHookImpl.Enable(); + lock (HookManager.HookEnableSyncRoot) + { + this.minHookImpl.Enable(); + } } } @@ -97,7 +106,10 @@ namespace Dalamud.Hooking.Internal if (this.minHookImpl.Enabled) { - this.minHookImpl.Disable(); + lock (HookManager.HookEnableSyncRoot) + { + this.minHookImpl.Disable(); + } } } } diff --git a/Dalamud/Hooking/Internal/ReloadedHook.cs b/Dalamud/Hooking/Internal/ReloadedHook.cs index 4b3acd16d..d82b452a3 100644 --- a/Dalamud/Hooking/Internal/ReloadedHook.cs +++ b/Dalamud/Hooking/Internal/ReloadedHook.cs @@ -19,16 +19,19 @@ namespace Dalamud.Hooking.Internal internal ReloadedHook(IntPtr address, T detour, Assembly callingAssembly) : base(address) { - var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); - if (!hasOtherHooks) + lock (HookManager.HookEnableSyncRoot) { - MemoryHelper.ReadRaw(this.Address, 0x32, out var original); - HookManager.Originals[this.Address] = original; + var hasOtherHooks = HookManager.Originals.ContainsKey(this.Address); + if (!hasOtherHooks) + { + MemoryHelper.ReadRaw(this.Address, 0x32, out var original); + HookManager.Originals[this.Address] = original; + } + + this.hookImpl = ReloadedHooks.Instance.CreateHook(detour, address.ToInt64()); + + HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } - - this.hookImpl = ReloadedHooks.Instance.CreateHook(detour, address.ToInt64()); - - HookManager.TrackedHooks.TryAdd(Guid.NewGuid(), new HookInfo(this, detour, callingAssembly)); } /// @@ -70,11 +73,14 @@ namespace Dalamud.Hooking.Internal { this.CheckDisposed(); - if (!this.hookImpl.IsHookActivated) - this.hookImpl.Activate(); + lock (HookManager.HookEnableSyncRoot) + { + if (!this.hookImpl.IsHookActivated) + this.hookImpl.Activate(); - if (!this.hookImpl.IsHookEnabled) - this.hookImpl.Enable(); + if (!this.hookImpl.IsHookEnabled) + this.hookImpl.Enable(); + } } /// @@ -82,11 +88,14 @@ namespace Dalamud.Hooking.Internal { this.CheckDisposed(); - if (!this.hookImpl.IsHookActivated) - return; + lock (HookManager.HookEnableSyncRoot) + { + if (!this.hookImpl.IsHookActivated) + return; - if (this.hookImpl.IsHookEnabled) - this.hookImpl.Disable(); + if (this.hookImpl.IsHookEnabled) + this.hookImpl.Disable(); + } } } } diff --git a/Dalamud/Interface/GameFonts/GameFontManager.cs b/Dalamud/Interface/GameFonts/GameFontManager.cs index 28214d887..a9bdd73f8 100644 --- a/Dalamud/Interface/GameFonts/GameFontManager.cs +++ b/Dalamud/Interface/GameFonts/GameFontManager.cs @@ -18,7 +18,7 @@ namespace Dalamud.Interface.GameFonts /// Loads game font for use in ImGui. /// [ServiceManager.EarlyLoadedService] - internal class GameFontManager : IDisposable, IServiceType + internal class GameFontManager : IServiceType { private static readonly string?[] FontNames = { @@ -158,11 +158,6 @@ namespace Dalamud.Interface.GameFonts fontPtr.BuildLookupTable(); } - /// - public void Dispose() - { - } - /// /// Creates a new GameFontHandle, and increases internal font reference counter, and if it's first time use, then the font will be loaded on next font building process. /// diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index e5d16289e..5a6bae722 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -60,8 +60,6 @@ namespace Dalamud.Interface.Internal private readonly TextureWrap logoTexture; private readonly TextureWrap tsmLogoTexture; - private ulong frameCount = 0; - #if DEBUG private bool isImGuiDrawDevMenu = true; #else @@ -80,7 +78,8 @@ namespace Dalamud.Interface.Internal private DalamudInterface( Dalamud dalamud, DalamudConfiguration configuration, - InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene) + InterfaceManager.InterfaceManagerWithScene interfaceManagerWithScene, + PluginImageCache pluginImageCache) { var interfaceManager = interfaceManagerWithScene.Manager; this.WindowSystem = new WindowSystem("DalamudCore"); @@ -94,7 +93,7 @@ namespace Dalamud.Interface.Internal this.imeWindow = new IMEWindow() { IsOpen = false }; this.consoleWindow = new ConsoleWindow() { IsOpen = configuration.LogOpenAtStartup }; this.pluginStatWindow = new PluginStatWindow() { IsOpen = false }; - this.pluginWindow = new PluginInstallerWindow() { IsOpen = false }; + this.pluginWindow = new PluginInstallerWindow(pluginImageCache) { IsOpen = false }; this.settingsWindow = new SettingsWindow() { IsOpen = false }; this.selfTestWindow = new SelfTestWindow() { IsOpen = false }; this.styleEditorWindow = new StyleEditorWindow() { IsOpen = false }; @@ -138,15 +137,20 @@ namespace Dalamud.Interface.Internal this.tsmLogoTexture = tsmLogoTex; var tsm = Service.Get(); - tsm.AddEntry(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true); - tsm.AddEntry(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true); + tsm.AddEntryCore(Loc.Localize("TSMDalamudPlugins", "Plugin Installer"), this.tsmLogoTexture, () => this.pluginWindow.IsOpen = true); + tsm.AddEntryCore(Loc.Localize("TSMDalamudSettings", "Dalamud Settings"), this.tsmLogoTexture, () => this.settingsWindow.IsOpen = true); if (configuration.IsConventionalStaging) { - tsm.AddEntry(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true); + tsm.AddEntryCore(Loc.Localize("TSMDalamudDevMenu", "Developer Menu"), this.tsmLogoTexture, () => this.isImGuiDrawDevMenu = true); } } + /// + /// Gets the number of frames since Dalamud has loaded. + /// + public ulong FrameCount { get; private set; } + /// /// Gets the controlling all Dalamud-internal windows. /// @@ -373,7 +377,7 @@ namespace Dalamud.Interface.Internal private void OnDraw() { - this.frameCount++; + this.FrameCount++; #if BOOT_AGING if (this.frameCount > 500 && !this.signaledBoot) @@ -494,9 +498,9 @@ namespace Dalamud.Interface.Internal { foreach (var logLevel in Enum.GetValues(typeof(LogEventLevel)).Cast()) { - if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, dalamud.LogLevelSwitch.MinimumLevel == logLevel)) + if (ImGui.MenuItem(logLevel + "##logLevelSwitch", string.Empty, EntryPoint.LogLevelSwitch.MinimumLevel == logLevel)) { - dalamud.LogLevelSwitch.MinimumLevel = logLevel; + EntryPoint.LogLevelSwitch.MinimumLevel = logLevel; configuration.LogLevel = logLevel; configuration.Save(); } @@ -505,6 +509,19 @@ namespace Dalamud.Interface.Internal ImGui.EndMenu(); } + var logSynchronously = configuration.LogSynchronously; + if (ImGui.MenuItem("Log Synchronously", null, ref logSynchronously)) + { + configuration.LogSynchronously = logSynchronously; + configuration.Save(); + + var startupInfo = Service.Get(); + EntryPoint.InitLogging( + startupInfo.WorkingDirectory!, + startupInfo.BootShowConsole, + configuration.LogSynchronously); + } + var antiDebug = Service.Get(); if (ImGui.MenuItem("Enable AntiDebug", null, antiDebug.IsEnabled)) { @@ -584,7 +601,7 @@ namespace Dalamud.Interface.Internal Marshal.ReadByte(IntPtr.Zero); } - if (ImGui.MenuItem("Crash game")) + if (ImGui.MenuItem("Crash game (nullptr)")) { unsafe { @@ -593,6 +610,15 @@ namespace Dalamud.Interface.Internal } } + if (ImGui.MenuItem("Crash game (non-nullptr)")) + { + unsafe + { + var framework = Framework.Instance(); + framework->UIModule = (UIModule*)0x12345678; + } + } + ImGui.Separator(); var isBeta = configuration.DalamudBetaKey == DalamudConfiguration.DalamudCurrentBetaKey; @@ -795,7 +821,7 @@ namespace Dalamud.Interface.Internal ImGui.PushFont(InterfaceManager.MonoFont); ImGui.BeginMenu(Util.GetGitHash(), false); - ImGui.BeginMenu(this.frameCount.ToString("000000"), false); + ImGui.BeginMenu(this.FrameCount.ToString("000000"), false); ImGui.BeginMenu(ImGui.GetIO().Framerate.ToString("000"), false); ImGui.BeginMenu($"{Util.FormatBytes(GC.GetTotalMemory(false))}", false); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index e73783aa4..20bb86e3c 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -57,6 +57,9 @@ namespace Dalamud.Interface.Internal private readonly HashSet glyphRequests = new(); private readonly Dictionary loadedFontInfo = new(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly ManualResetEvent fontBuildSignal; private readonly SwapChainVtableResolver address; private readonly Hook dispatchMessageWHook; @@ -73,12 +76,12 @@ namespace Dalamud.Interface.Internal private bool isOverrideGameCursor = true; [ServiceManager.ServiceConstructor] - private InterfaceManager(SigScanner sigScanner) + private InterfaceManager() { this.dispatchMessageWHook = Hook.FromImport( - Process.GetCurrentProcess().MainModule, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); + null, "user32.dll", "DispatchMessageW", 0, this.DispatchMessageWDetour); this.setCursorHook = Hook.FromImport( - Process.GetCurrentProcess().MainModule, "user32.dll", "SetCursor", 0, this.SetCursorDetour); + null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); this.fontBuildSignal = new ManualResetEvent(false); @@ -91,12 +94,6 @@ namespace Dalamud.Interface.Internal [UnmanagedFunctionPointer(CallingConvention.ThisCall)] private delegate IntPtr ResizeBuffersDelegate(IntPtr swapChain, uint bufferCount, uint width, uint height, uint newFormat, uint swapChainFlags); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate int CreateDXGIFactoryDelegate(Guid riid, out IntPtr ppFactory); - - [UnmanagedFunctionPointer(CallingConvention.StdCall)] - private delegate int IDXGIFactory_CreateSwapChainDelegate(IntPtr pFactory, IntPtr pDevice, IntPtr pDesc, out IntPtr ppSwapChain); - [UnmanagedFunctionPointer(CallingConvention.StdCall)] private delegate IntPtr SetCursorDelegate(IntPtr hCursor); @@ -248,20 +245,15 @@ namespace Dalamud.Interface.Internal /// public void Dispose() { - // HACK: this is usually called on a separate thread from PresentDetour (likely on a dedicated render thread) - // and if we aren't already disabled, disposing of the scene and hook can frequently crash due to the hook - // being disposed of in this thread while it is actively in use in the render thread. - // This is a terrible way to prevent issues, but should basically always work to ensure that all outstanding - // calls to PresentDetour have finished (and Disable means no new ones will start), before we try to cleanup - // So... not great, but much better than constantly crashing on unload - this.Disable(); - Thread.Sleep(500); + this.framework.RunOnFrameworkThread(() => + { + this.setCursorHook.Dispose(); + this.presentHook?.Dispose(); + this.resizeBuffersHook?.Dispose(); + this.dispatchMessageWHook.Dispose(); + }).Wait(); this.scene?.Dispose(); - this.setCursorHook.Dispose(); - this.presentHook?.Dispose(); - this.resizeBuffersHook?.Dispose(); - this.dispatchMessageWHook.Dispose(); } #nullable enable @@ -990,8 +982,8 @@ namespace Dalamud.Interface.Internal break; } - this.presentHook = new Hook(this.address.Present, this.PresentDetour); - this.resizeBuffersHook = new Hook(this.address.ResizeBuffers, this.ResizeBuffersDetour); + this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); + this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); Log.Verbose("===== S W A P C H A I N ====="); Log.Verbose($"Present address 0x{this.presentHook!.Address.ToInt64():X}"); @@ -1004,14 +996,6 @@ namespace Dalamud.Interface.Internal }); } - private void Disable() - { - this.setCursorHook.Disable(); - this.presentHook?.Disable(); - this.resizeBuffersHook?.Disable(); - this.dispatchMessageWHook.Disable(); - } - // This is intended to only be called as a handler attached to scene.OnNewRenderFrame private void RebuildFontsInternal() { @@ -1104,7 +1088,7 @@ namespace Dalamud.Interface.Internal if (this.lastWantCapture == true && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook!.Original(hCursor); + return this.setCursorHook.IsDisposed ? User32.SetCursor(new User32.SafeCursorHandle(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } private void OnNewInputFrame() diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 26e1d63ad..587ddba7e 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -123,7 +123,6 @@ namespace Dalamud.Interface.Internal.Windows // Options menu if (ImGui.BeginPopup("Options")) { - var dalamud = Service.Get(); var configuration = Service.Get(); if (ImGui.Checkbox("Auto-scroll", ref this.autoScroll)) @@ -138,10 +137,10 @@ namespace Dalamud.Interface.Internal.Windows configuration.Save(); } - var prevLevel = (int)dalamud.LogLevelSwitch.MinimumLevel; + var prevLevel = (int)EntryPoint.LogLevelSwitch.MinimumLevel; if (ImGui.Combo("Log Level", ref prevLevel, Enum.GetValues(typeof(LogEventLevel)).Cast().Select(x => x.ToString()).ToArray(), 6)) { - dalamud.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; + EntryPoint.LogLevelSwitch.MinimumLevel = (LogEventLevel)prevLevel; configuration.LogLevel = (LogEventLevel)prevLevel; configuration.Save(); } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 2e714a8c8..ccc10ac3f 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -7,7 +7,6 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; - using Dalamud.Game; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Internal.Types; @@ -20,7 +19,8 @@ namespace Dalamud.Interface.Internal.Windows /// /// A cache for plugin icons and images. /// - internal class PluginImageCache : IDisposable + [ServiceManager.EarlyLoadedService] + internal class PluginImageCache : IDisposable, IServiceType { /// /// Maximum plugin image width. @@ -44,102 +44,133 @@ namespace Dalamud.Interface.Internal.Windows private const string MainRepoImageUrl = "https://raw.githubusercontent.com/goatcorp/DalamudPlugins/api6/{0}/{1}/images/{2}"; - private BlockingCollection> downloadQueue = new(); - private BlockingCollection loadQueue = new(); - private CancellationTokenSource downloadToken = new(); - private Thread downloadThread; + private readonly BlockingCollection>> downloadQueue = new(); + private readonly BlockingCollection> loadQueue = new(); + private readonly CancellationTokenSource cancelToken = new(); + private readonly Task downloadTask; + private readonly Task loadTask; - private Dictionary pluginIconMap = new(); - private Dictionary pluginImagesMap = new(); + private readonly Dictionary pluginIconMap = new(); + private readonly Dictionary pluginImagesMap = new(); + + private readonly Task emptyTextureTask; + private readonly Task defaultIconTask; + private readonly Task troubleIconTask; + private readonly Task updateIconTask; + private readonly Task installedIconTask; + private readonly Task thirdIconTask; + private readonly Task thirdInstalledIconTask; + private readonly Task corePluginIconTask; + + [ServiceManager.ServiceConstructor] + private PluginImageCache(Dalamud dalamud) + { + var imwst = Service.GetAsync(); + + Task? TaskWrapIfNonNull(TextureWrap? tw) => tw == null ? null : Task.FromResult(tw!); + + this.emptyTextureTask = imwst.ContinueWith(task => task.Result.Manager.LoadImageRaw(new byte[64], 8, 8, 4)!); + this.defaultIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.troubleIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.updateIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.installedIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.thirdIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.thirdInstalledIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))) ?? this.emptyTextureTask).Unwrap(); + this.corePluginIconTask = imwst.ContinueWith(task => TaskWrapIfNonNull(task.Result.Manager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))) ?? this.emptyTextureTask).Unwrap(); + + this.downloadTask = Task.Factory.StartNew( + () => this.DownloadTask(8), TaskCreationOptions.LongRunning); + this.loadTask = Task.Factory.StartNew( + () => this.LoadTask(Environment.ProcessorCount), TaskCreationOptions.LongRunning); + } /// - /// Initializes a new instance of the class. + /// Gets the fallback empty texture. /// - public PluginImageCache() - { - var dalamud = Service.Get(); - var interfaceManager = Service.Get(); - - this.DefaultIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "defaultIcon.png"))!; - this.TroubleIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "troubleIcon.png"))!; - this.UpdateIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "updateIcon.png"))!; - this.InstalledIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "installedIcon.png"))!; - this.ThirdIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdIcon.png"))!; - this.ThirdInstalledIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "thirdInstalledIcon.png"))!; - this.CorePluginIcon = interfaceManager.LoadImage(Path.Combine(dalamud.AssetDirectory.FullName, "UIRes", "tsmLogo.png"))!; - - if (this.DefaultIcon == null || this.TroubleIcon == null || this.UpdateIcon == null || this.InstalledIcon == null || - this.ThirdIcon == null || this.ThirdInstalledIcon == null || this.CorePluginIcon == null) - { - throw new Exception("Plugin overlay images could not be loaded."); - } - - this.downloadThread = new Thread(this.DownloadTask); - this.downloadThread.Start(); - - var framework = Service.Get(); - framework.Update += this.FrameworkOnUpdate; - } + public TextureWrap EmptyTexture => this.emptyTextureTask.IsCompleted + ? this.emptyTextureTask.Result + : this.emptyTextureTask.GetAwaiter().GetResult(); /// /// Gets the default plugin icon. /// - public TextureWrap DefaultIcon { get; } + public TextureWrap DefaultIcon => this.defaultIconTask.IsCompleted + ? this.defaultIconTask.Result + : this.defaultIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin trouble icon overlay. /// - public TextureWrap TroubleIcon { get; } + public TextureWrap TroubleIcon => this.troubleIconTask.IsCompleted + ? this.troubleIconTask.Result + : this.troubleIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin update icon overlay. /// - public TextureWrap UpdateIcon { get; } + public TextureWrap UpdateIcon => this.updateIconTask.IsCompleted + ? this.updateIconTask.Result + : this.updateIconTask.GetAwaiter().GetResult(); /// /// Gets the plugin installed icon overlay. /// - public TextureWrap InstalledIcon { get; } + public TextureWrap InstalledIcon => this.installedIconTask.IsCompleted + ? this.installedIconTask.Result + : this.installedIconTask.GetAwaiter().GetResult(); /// /// Gets the third party plugin icon overlay. /// - public TextureWrap ThirdIcon { get; } + public TextureWrap ThirdIcon => this.thirdIconTask.IsCompleted + ? this.thirdIconTask.Result + : this.thirdIconTask.GetAwaiter().GetResult(); /// /// Gets the installed third party plugin icon overlay. /// - public TextureWrap ThirdInstalledIcon { get; } + public TextureWrap ThirdInstalledIcon => this.thirdInstalledIconTask.IsCompleted + ? this.thirdInstalledIconTask.Result + : this.thirdInstalledIconTask.GetAwaiter().GetResult(); /// /// Gets the core plugin icon. /// - public TextureWrap CorePluginIcon { get; } + public TextureWrap CorePluginIcon => this.corePluginIconTask.IsCompleted + ? this.corePluginIconTask.Result + : this.corePluginIconTask.GetAwaiter().GetResult(); /// public void Dispose() { - var framework = Service.Get(); - framework.Update -= this.FrameworkOnUpdate; + this.cancelToken.Cancel(); + this.downloadQueue.CompleteAdding(); + this.loadQueue.CompleteAdding(); - this.DefaultIcon?.Dispose(); - this.TroubleIcon?.Dispose(); - this.UpdateIcon?.Dispose(); - this.InstalledIcon?.Dispose(); - this.ThirdIcon?.Dispose(); - this.ThirdInstalledIcon?.Dispose(); - this.CorePluginIcon?.Dispose(); - - this.downloadToken?.Cancel(); - - if (!this.downloadThread.Join(4000)) + if (!Task.WaitAll(new[] { this.loadTask, this.downloadTask }, 4000)) { - Log.Error("Plugin Image Download thread has not cancelled in time"); + Log.Error("Plugin Image download/load thread has not cancelled in time"); } - this.downloadToken?.Dispose(); - this.downloadQueue?.CompleteAdding(); - this.downloadQueue?.Dispose(); + this.cancelToken.Dispose(); + this.downloadQueue.Dispose(); + this.loadQueue.Dispose(); + + foreach (var task in new[] + { + this.defaultIconTask, + this.troubleIconTask, + this.updateIconTask, + this.installedIconTask, + this.thirdIconTask, + this.thirdInstalledIconTask, + this.corePluginIconTask, + }) + { + task.Wait(); + if (task.IsCompletedSuccessfully) + task.Result.Dispose(); + } foreach (var icon in this.pluginIconMap.Values) { @@ -181,12 +212,22 @@ namespace Dalamud.Interface.Internal.Windows if (this.pluginIconMap.TryGetValue(manifest.InternalName, out iconTexture)) return true; - iconTexture = null; - this.pluginIconMap.Add(manifest.InternalName, iconTexture); + this.pluginIconMap.Add(manifest.InternalName, null); - if (!this.downloadQueue.IsCompleted) + try { - this.downloadQueue.Add(async () => await this.DownloadPluginIconAsync(plugin, manifest, isThirdParty)); + if (!this.downloadQueue.IsCompleted) + { + this.downloadQueue.Add( + Tuple.Create( + Service.GetNullable()?.FrameCount ?? 0, + () => this.DownloadPluginIconAsync(plugin, manifest, isThirdParty)), + this.cancelToken.Token); + } + } + catch (ObjectDisposedException) + { + // pass } return false; @@ -209,39 +250,120 @@ namespace Dalamud.Interface.Internal.Windows imageTextures = Array.Empty(); this.pluginImagesMap.Add(manifest.InternalName, imageTextures); - if (!this.downloadQueue.IsCompleted) + try { - this.downloadQueue.Add(async () => await this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)); + if (!this.downloadQueue.IsCompleted) + { + this.downloadQueue.Add( + Tuple.Create( + Service.GetNullable()?.FrameCount ?? 0, + () => this.DownloadPluginImagesAsync(plugin, manifest, isThirdParty)), + this.cancelToken.Token); + } + } + catch (ObjectDisposedException) + { + // pass } return false; } - private void FrameworkOnUpdate(Framework framework) + private static async Task TryLoadIcon( + byte[] bytes, + string name, + string? loc, + PluginManifest manifest, + int maxWidth, + int maxHeight, + bool requireSquare) { + var interfaceManager = (await Service.GetAsync()).Manager; + var framework = await Service.GetAsync(); + + TextureWrap? icon; + // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. try { - if (!this.loadQueue.TryTake(out var loadAction, 0, this.downloadToken.Token)) - return; - - loadAction.Invoke(); + icon = interfaceManager.LoadImage(bytes); } catch (Exception ex) { - Log.Error(ex, "An unhandled exception occurred in image loader framework dispatcher"); + Log.Error(ex, "Access violation during load plugin {name} from {Loc} (Async Thread)", name, loc); + + try + { + icon = await framework.RunOnFrameworkThread(() => interfaceManager.LoadImage(bytes)); + } + catch (Exception ex2) + { + Log.Error(ex2, "Access violation during load plugin {name} from {Loc} (Framework Thread)", name, loc); + return null; + } } + + if (icon == null) + { + Log.Error($"Could not load {name} for {manifest.InternalName} at {loc}"); + return null; + } + + if (icon.Width > maxWidth || icon.Height > maxHeight) + { + Log.Error($"Plugin {name} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({maxWidth}x{maxHeight})."); + icon.Dispose(); + return null; + } + + if (requireSquare && icon.Height != icon.Width) + { + Log.Error($"Plugin {name} for {manifest.InternalName} at {loc} was not square."); + icon.Dispose(); + return null; + } + + return icon!; } - private async void DownloadTask() + private async Task DownloadTask(int concurrency) { - while (!this.downloadToken.Token.IsCancellationRequested) + var token = this.cancelToken.Token; + var runningTasks = new List(); + var pendingFuncs = new List>>(); + while (true) { try { - if (!this.downloadQueue.TryTake(out var task, -1, this.downloadToken.Token)) - return; + token.ThrowIfCancellationRequested(); + if (!pendingFuncs.Any()) + { + if (!this.downloadQueue.TryTake(out var taskTuple, -1, token)) + return; - await task.Invoke(); + pendingFuncs.Add(taskTuple); + } + + token.ThrowIfCancellationRequested(); + while (this.downloadQueue.TryTake(out var taskTuple, 0, token)) + pendingFuncs.Add(taskTuple); + + // Process most recently requested items first in terms of frame index. + pendingFuncs = pendingFuncs.OrderBy(x => x.Item1).ToList(); + + var item1 = pendingFuncs.Last().Item1; + while (pendingFuncs.Any() && pendingFuncs.Last().Item1 == item1) + { + token.ThrowIfCancellationRequested(); + while (runningTasks.Count >= concurrency) + { + await Task.WhenAny(runningTasks); + runningTasks.RemoveAll(task => task.IsCompleted); + } + + token.ThrowIfCancellationRequested(); + runningTasks.Add(Task.Run(pendingFuncs.Last().Item2, token)); + pendingFuncs.RemoveAt(pendingFuncs.Count - 1); + } } catch (OperationCanceledException) { @@ -252,52 +374,56 @@ namespace Dalamud.Interface.Internal.Windows { Log.Error(ex, "An unhandled exception occurred in the plugin image downloader"); } + + while (runningTasks.Count >= concurrency) + { + await Task.WhenAny(runningTasks); + runningTasks.RemoveAll(task => task.IsCompleted); + } } + await Task.WhenAll(runningTasks); Log.Debug("Plugin image downloader has shutdown"); } + private async Task LoadTask(int concurrency) + { + await Service.GetAsync(); + + var token = this.cancelToken.Token; + var runningTasks = new List(); + while (true) + { + try + { + token.ThrowIfCancellationRequested(); + while (runningTasks.Count >= concurrency) + { + await Task.WhenAny(runningTasks); + runningTasks.RemoveAll(task => task.IsCompleted); + } + + if (!this.loadQueue.TryTake(out var func, -1, token)) + return; + runningTasks.Add(Task.Run(func, token)); + } + catch (OperationCanceledException) + { + // Shutdown signal. + break; + } + catch (Exception ex) + { + Log.Error(ex, "An unhandled exception occurred in the plugin image loader"); + } + } + + await Task.WhenAll(runningTasks); + Log.Debug("Plugin image loader has shutdown"); + } + private async Task DownloadPluginIconAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) { - var interfaceManager = Service.Get(); - var pluginManager = Service.Get(); - - static bool TryLoadIcon(byte[] bytes, string? loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap? icon) - { - // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. - try - { - icon = interfaceManager.LoadImage(bytes); - } - catch (AccessViolationException ex) - { - Log.Error(ex, "Access violation during load plugin icon from {Loc}", loc); - - icon = null; - return false; - } - - if (icon == null) - { - Log.Error($"Could not load icon for {manifest.InternalName} at {loc}"); - return false; - } - - if (icon.Width > PluginIconWidth || icon.Height > PluginIconHeight) - { - Log.Error($"Icon for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginIconWidth}x{PluginIconHeight})."); - return false; - } - - if (icon.Height != icon.Width) - { - Log.Error($"Icon for {manifest.InternalName} at {loc} was not square."); - return false; - } - - return true; - } - if (plugin != null && plugin.IsDev) { var file = this.GetPluginIconFileInfo(plugin); @@ -306,7 +432,8 @@ namespace Dalamud.Interface.Internal.Windows Log.Verbose($"Fetching icon for {manifest.InternalName} from {file.FullName}"); var bytes = await File.ReadAllBytesAsync(file.FullName); - if (!TryLoadIcon(bytes, file.FullName, manifest, interfaceManager, out var icon)) + var icon = await TryLoadIcon(bytes, "icon", file.FullName, manifest, PluginIconWidth, PluginIconHeight, true); + if (icon == null) return; this.pluginIconMap[manifest.InternalName] = icon; @@ -349,9 +476,10 @@ namespace Dalamud.Interface.Internal.Windows data.EnsureSuccessStatusCode(); var bytes = await data.Content.ReadAsByteArrayAsync(); - this.loadQueue.Add(() => + this.loadQueue.Add(async () => { - if (!TryLoadIcon(bytes, url, manifest, interfaceManager, out var icon)) + var icon = await TryLoadIcon(bytes, "icon", url, manifest, PluginIconWidth, PluginIconHeight, true); + if (icon == null) return; this.pluginIconMap[manifest.InternalName] = icon; @@ -366,39 +494,6 @@ namespace Dalamud.Interface.Internal.Windows private async Task DownloadPluginImagesAsync(LocalPlugin? plugin, PluginManifest manifest, bool isThirdParty) { - var interfaceManager = Service.Get(); - var pluginManager = Service.Get(); - - static bool TryLoadImage(int i, byte[] bytes, string loc, PluginManifest manifest, InterfaceManager interfaceManager, out TextureWrap? image) - { - // FIXME(goat): This is a hack around this call failing randomly in certain situations. Might be related to not being called on the main thread. - try - { - image = interfaceManager.LoadImage(bytes); - } - catch (AccessViolationException ex) - { - Log.Error(ex, "Access violation during load plugin image from {Loc}", loc); - - image = null; - return false; - } - - if (image == null) - { - Log.Error($"Could not load image{i + 1} for {manifest.InternalName} at {loc}"); - return false; - } - - if (image.Width > PluginImageWidth || image.Height > PluginImageHeight) - { - Log.Error($"Plugin image{i + 1} for {manifest.InternalName} at {loc} was larger than the maximum allowed resolution ({PluginImageWidth}x{PluginImageHeight})."); - return false; - } - - return true; - } - if (plugin is { IsDev: true }) { var files = this.GetPluginImageFileInfos(plugin); @@ -415,7 +510,8 @@ namespace Dalamud.Interface.Internal.Windows Log.Verbose($"Loading image{i + 1} for {manifest.InternalName} from {file.FullName}"); var bytes = await File.ReadAllBytesAsync(file.FullName); - if (!TryLoadImage(i, bytes, file.FullName, manifest, interfaceManager, out var image)) + var image = await TryLoadIcon(bytes, $"image{i + 1}", file.FullName, manifest, PluginImageWidth, PluginImageHeight, true); + if (image == null) continue; Log.Verbose($"Plugin image{i + 1} for {manifest.InternalName} loaded from disk"); @@ -490,17 +586,16 @@ namespace Dalamud.Interface.Internal.Windows if (didAny) { - this.loadQueue.Add(() => + this.loadQueue.Add(async () => { var pluginImages = new TextureWrap[urls.Count]; for (var i = 0; i < imageBytes.Length; i++) { var bytes = imageBytes[i]; - if (bytes == null) - continue; - if (!TryLoadImage(i, bytes, "queue", manifest, interfaceManager, out var image)) + var image = await TryLoadIcon(bytes, $"image{i + 1}", "queue", manifest, PluginImageWidth, PluginImageHeight, true); + if (image == null) continue; pluginImages[i] = image; @@ -551,7 +646,9 @@ namespace Dalamud.Interface.Internal.Windows private FileInfo? GetPluginIconFileInfo(LocalPlugin? plugin) { - var pluginDir = plugin.DllFile.Directory; + var pluginDir = plugin?.DllFile.Directory; + if (pluginDir == null) + return null; var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", "icon.png")); if (devUrl.Exists) @@ -562,8 +659,12 @@ namespace Dalamud.Interface.Internal.Windows private List GetPluginImageFileInfos(LocalPlugin? plugin) { - var pluginDir = plugin.DllFile.Directory; var output = new List(); + + var pluginDir = plugin?.DllFile.Directory; + if (pluginDir == null) + return output; + for (var i = 1; i <= 5; i++) { var devUrl = new FileInfo(Path.Combine(pluginDir.FullName, "images", $"image{i}.png")); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 7f02781a5..a570bc33b 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -37,8 +37,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller private readonly Vector4 changelogBgColor = new(0.114f, 0.584f, 0.192f, 0.678f); private readonly Vector4 changelogTextColor = new(0.812f, 1.000f, 0.816f, 1.000f); + private readonly PluginImageCache imageCache; private readonly PluginCategoryManager categoryManager = new(); - private readonly PluginImageCache imageCache = new(); private readonly DalamudChangelogManager dalamudChangelogManager = new(); private readonly List openPluginCollapsibles = new(); @@ -87,12 +87,14 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller /// /// Initializes a new instance of the class. /// - public PluginInstallerWindow() + /// An instance of class. + public PluginInstallerWindow(PluginImageCache imageCache) : base( Locs.WindowTitle + (Service.Get().DoPluginTest ? Locs.WindowTitleMod_Testing : string.Empty) + "###XlPluginInstaller", ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoScrollbar) { this.IsOpen = true; + this.imageCache = imageCache; this.Size = new Vector2(830, 570); this.SizeCondition = ImGuiCond.FirstUseEver; @@ -200,6 +202,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller ImGui.SetCursorPosY(ImGui.GetCursorPosY() - (5 * ImGuiHelpers.GlobalScale)); var searchInputWidth = 240 * ImGuiHelpers.GlobalScale; + var searchClearButtonWidth = 40 * ImGuiHelpers.GlobalScale; var sortByText = Locs.SortBy_Label; var sortByTextWidth = ImGui.CalcTextSize(sortByText).X; @@ -224,13 +227,28 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller // Shift down a little to align with the middle of the header text ImGui.SetCursorPosY(ImGui.GetCursorPosY() + (headerTextSize.Y / 4) - 2); - ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth); + ImGui.SetCursorPosX(windowSize.X - sortSelectWidth - style.ItemSpacing.X - searchInputWidth - searchClearButtonWidth); + + var searchTextChanged = false; ImGui.SetNextItemWidth(searchInputWidth); - if (ImGui.InputTextWithHint("###XlPluginInstaller_Search", Locs.Header_SearchPlaceholder, ref this.searchText, 100)) + searchTextChanged |= ImGui.InputTextWithHint( + "###XlPluginInstaller_Search", + Locs.Header_SearchPlaceholder, + ref this.searchText, + 100); + + ImGui.SameLine(); + + ImGui.SetNextItemWidth(searchClearButtonWidth); + if (ImGuiComponents.IconButton(FontAwesomeIcon.Times)) { - this.UpdateCategoriesOnSearchChange(); + this.searchText = string.Empty; + searchTextChanged = true; } + if (searchTextChanged) + this.UpdateCategoriesOnSearchChange(); + ImGui.SameLine(); ImGui.SetCursorPosX(windowSize.X - sortSelectWidth); ImGui.SetNextItemWidth(selectableWidth); @@ -552,7 +570,8 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller var i = 0; foreach (var manifest in categoryManifestsList) { - var remoteManifest = manifest as RemotePluginManifest; + if (manifest is not RemotePluginManifest remoteManifest) + continue; var (isInstalled, plugin) = this.IsManifestInstalled(remoteManifest); ImGui.PushID($"{manifest.InternalName}{manifest.AssemblyVersion}"); @@ -1087,51 +1106,38 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller ImGui.SetCursorPos(startCursor); - var iconTex = this.imageCache.DefaultIcon; - var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex); - if (hasIcon && cachedIconTex != null) - { - iconTex = cachedIconTex; - } - var iconSize = ImGuiHelpers.ScaledVector2(64, 64); - var cursorBeforeImage = ImGui.GetCursorPos(); - ImGui.Image(iconTex.ImGuiHandle, iconSize); - ImGui.SameLine(); + var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); + if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) + { + var iconTex = this.imageCache.DefaultIcon; + var hasIcon = this.imageCache.TryGetIcon(plugin, manifest, isThirdParty, out var cachedIconTex); + if (hasIcon && cachedIconTex != null) + { + iconTex = cachedIconTex; + } + + ImGui.Image(iconTex.ImGuiHandle, iconSize); + ImGui.SameLine(); + ImGui.SetCursorPos(cursorBeforeImage); + } var isLoaded = plugin is { IsLoaded: true }; if (updateAvailable) - { - ImGui.SetCursorPos(cursorBeforeImage); ImGui.Image(this.imageCache.UpdateIcon.ImGuiHandle, iconSize); - ImGui.SameLine(); - } else if (trouble) - { - ImGui.SetCursorPos(cursorBeforeImage); ImGui.Image(this.imageCache.TroubleIcon.ImGuiHandle, iconSize); - ImGui.SameLine(); - } else if (isLoaded && isThirdParty) - { - ImGui.SetCursorPos(cursorBeforeImage); ImGui.Image(this.imageCache.ThirdInstalledIcon.ImGuiHandle, iconSize); - ImGui.SameLine(); - } else if (isThirdParty) - { - ImGui.SetCursorPos(cursorBeforeImage); ImGui.Image(this.imageCache.ThirdIcon.ImGuiHandle, iconSize); - ImGui.SameLine(); - } else if (isLoaded) - { - ImGui.SetCursorPos(cursorBeforeImage); ImGui.Image(this.imageCache.InstalledIcon.ImGuiHandle, iconSize); - ImGui.SameLine(); - } + else + ImGui.Dummy(iconSize); + ImGui.SameLine(); ImGuiHelpers.ScaledDummy(5); ImGui.SameLine(); @@ -1208,23 +1214,32 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller var startCursor = ImGui.GetCursorPos(); var iconSize = ImGuiHelpers.ScaledVector2(64, 64); - - TextureWrap icon; - if (log is PluginChangelogEntry pluginLog) + var cursorBeforeImage = ImGui.GetCursorPos(); + var rectOffset = ImGui.GetWindowContentRegionMin() + ImGui.GetWindowPos(); + if (ImGui.IsRectVisible(rectOffset + cursorBeforeImage, rectOffset + cursorBeforeImage + iconSize)) { - icon = this.imageCache.DefaultIcon; - var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex); - if (hasIcon && cachedIconTex != null) + TextureWrap icon; + if (log is PluginChangelogEntry pluginLog) { - icon = cachedIconTex; + icon = this.imageCache.DefaultIcon; + var hasIcon = this.imageCache.TryGetIcon(pluginLog.Plugin, pluginLog.Plugin.Manifest, pluginLog.Plugin.Manifest.IsThirdParty, out var cachedIconTex); + if (hasIcon && cachedIconTex != null) + { + icon = cachedIconTex; + } } + else + { + icon = this.imageCache.CorePluginIcon; + } + + ImGui.Image(icon.ImGuiHandle, iconSize); } else { - icon = this.imageCache.CorePluginIcon; + ImGui.Dummy(iconSize); } - ImGui.Image(icon.ImGuiHandle, iconSize); ImGui.SameLine(); ImGuiHelpers.ScaledDummy(5); @@ -1644,7 +1659,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller this.installStatus = OperationStatus.InProgress; - Task.Run(() => pluginManager.DeleteConfiguration(plugin)) + Task.Run(() => pluginManager.DeleteConfigurationAsync(plugin)) .ContinueWith(task => { this.installStatus = OperationStatus.Idle; @@ -1670,7 +1685,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller // Disable everything if the plugin is outdated disabled = disabled || (plugin.IsOutdated && !configuration.LoadAllApiLevels) || plugin.IsBanned; - if (plugin.State == PluginState.InProgress) + if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloading) { ImGuiComponents.DisabledButton(Locs.PluginButton_Working); } @@ -1686,7 +1701,7 @@ namespace Dalamud.Interface.Internal.Windows.PluginInstaller { Task.Run(() => { - var unloadTask = Task.Run(() => plugin.Unload()) + var unloadTask = Task.Run(() => plugin.UnloadAsync()) .ContinueWith(this.DisplayErrorContinuation, Locs.ErrorModal_UnloadFail(plugin.Name)); unloadTask.Wait(); diff --git a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs index 86152d1e4..b24504889 100644 --- a/Dalamud/Interface/Internal/Windows/SettingsWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SettingsWindow.cs @@ -57,6 +57,8 @@ namespace Dalamud.Interface.Internal.Windows private int dtrSpacing; private bool dtrSwapDirection; + private int? pluginWaitBeforeFree; + private List thirdRepoList; private bool thirdRepoListChanged; private string thirdRepoTempUrl = string.Empty; @@ -113,6 +115,8 @@ namespace Dalamud.Interface.Internal.Windows this.dtrSpacing = configuration.DtrSpacing; this.dtrSwapDirection = configuration.DtrSwapDirection; + this.pluginWaitBeforeFree = configuration.PluginWaitBeforeFree; + this.doPluginTest = configuration.DoPluginTest; this.thirdRepoList = configuration.ThirdRepoList.Select(x => x.Clone()).ToList(); this.devPluginLocations = configuration.DevPluginLoadLocations.Select(x => x.Clone()).ToList(); @@ -561,6 +565,34 @@ namespace Dalamud.Interface.Internal.Windows var configuration = Service.Get(); var pluginManager = Service.Get(); + var useCustomPluginWaitBeforeFree = this.pluginWaitBeforeFree.HasValue; + if (ImGui.Checkbox( + Loc.Localize("DalamudSettingsPluginCustomizeWaitTime", "Customize wait time for plugin unload"), + ref useCustomPluginWaitBeforeFree)) + { + if (!useCustomPluginWaitBeforeFree) + this.pluginWaitBeforeFree = null; + else + this.pluginWaitBeforeFree = PluginManager.PluginWaitBeforeFreeDefault; + } + + if (useCustomPluginWaitBeforeFree) + { + var waitTime = this.pluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault; + if (ImGui.SliderInt( + "Wait time###DalamudSettingsPluginCustomizeWaitTimeSlider", + ref waitTime, + 0, + 5000)) + { + this.pluginWaitBeforeFree = waitTime; + } + } + + ImGui.TextColored(ImGuiColors.DalamudGrey, Loc.Localize("DalamudSettingsPluginCustomizeWaitTimeHint", "Configure the wait time between stopping plugin and completely unloading plugin. If you are experiencing crashes when exiting the game, try increasing this value.")); + + ImGuiHelpers.ScaledDummy(12); + #region Plugin testing ImGui.Checkbox(Loc.Localize("DalamudSettingsPluginTest", "Get plugin testing builds"), ref this.doPluginTest); @@ -974,6 +1006,8 @@ namespace Dalamud.Interface.Internal.Windows configuration.DtrSpacing = this.dtrSpacing; configuration.DtrSwapDirection = this.dtrSwapDirection; + configuration.PluginWaitBeforeFree = this.pluginWaitBeforeFree; + configuration.DoPluginTest = this.doPluginTest; configuration.ThirdRepoList = this.thirdRepoList.Select(x => x.Clone()).ToList(); configuration.DevPluginLoadLocations = this.devPluginLocations.Select(x => x.Clone()).ToList(); diff --git a/Dalamud/Interface/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu.cs index f44b112d7..5b1e204f8 100644 --- a/Dalamud/Interface/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; - +using System.Linq; +using System.Reflection; using Dalamud.IoC; using Dalamud.IoC.Internal; using ImGuiScene; @@ -47,37 +48,140 @@ namespace Dalamud.Interface throw new ArgumentException("Texture must be 64x64"); } - var entry = new TitleScreenMenuEntry(text, texture, onTriggered); - this.entries.Add(entry); - return entry; + lock (this.entries) + { + var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == Assembly.GetCallingAssembly()).ToList(); + var priority = entriesOfAssembly.Any() + ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) + : 0; + var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + var i = this.entries.BinarySearch(entry); + if (i < 0) + i = ~i; + this.entries.Insert(i, entry); + return entry; + } + } + + /// + /// Adds a new entry to the title screen menu. + /// + /// Priority of the entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + public TitleScreenMenuEntry AddEntry(ulong priority, string text, TextureWrap texture, Action onTriggered) + { + if (texture.Height != TextureSize || texture.Width != TextureSize) + { + throw new ArgumentException("Texture must be 64x64"); + } + + lock (this.entries) + { + var entry = new TitleScreenMenuEntry(Assembly.GetCallingAssembly(), priority, text, texture, onTriggered); + var i = this.entries.BinarySearch(entry); + if (i < 0) + i = ~i; + this.entries.Insert(i, entry); + return entry; + } } /// /// Remove an entry from the title screen menu. /// /// The entry to remove. - public void RemoveEntry(TitleScreenMenuEntry entry) => this.entries.Remove(entry); + public void RemoveEntry(TitleScreenMenuEntry entry) + { + lock (this.entries) + { + this.entries.Remove(entry); + } + } + + /// + /// Adds a new entry to the title screen menu. + /// + /// Priority of the entry. + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + internal TitleScreenMenuEntry AddEntryCore(ulong priority, string text, TextureWrap texture, Action onTriggered) + { + if (texture.Height != TextureSize || texture.Width != TextureSize) + { + throw new ArgumentException("Texture must be 64x64"); + } + + lock (this.entries) + { + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + this.entries.Add(entry); + return entry; + } + } + + /// + /// Adds a new entry to the title screen menu. + /// + /// The text to show. + /// The texture to show. + /// The action to execute when the option is selected. + /// A object that can be used to manage the entry. + /// Thrown when the texture provided does not match the required resolution(64x64). + internal TitleScreenMenuEntry AddEntryCore(string text, TextureWrap texture, Action onTriggered) + { + if (texture.Height != TextureSize || texture.Width != TextureSize) + { + throw new ArgumentException("Texture must be 64x64"); + } + + lock (this.entries) + { + var entriesOfAssembly = this.entries.Where(x => x.CallingAssembly == null).ToList(); + var priority = entriesOfAssembly.Any() + ? unchecked(entriesOfAssembly.Select(x => x.Priority).Max() + 1) + : 0; + var entry = new TitleScreenMenuEntry(null, priority, text, texture, onTriggered); + this.entries.Add(entry); + return entry; + } + } /// /// Class representing an entry in the title screen menu. /// - public class TitleScreenMenuEntry + public class TitleScreenMenuEntry : IComparable { private readonly Action onTriggered; /// /// Initializes a new instance of the class. /// + /// The calling assembly. + /// The priority of this entry. /// The text to show. /// The texture to show. /// The action to execute when the option is selected. - internal TitleScreenMenuEntry(string text, TextureWrap texture, Action onTriggered) + internal TitleScreenMenuEntry(Assembly? callingAssembly, ulong priority, string text, TextureWrap texture, Action onTriggered) { + this.CallingAssembly = callingAssembly; + this.Priority = priority; this.Name = text; this.Texture = texture; this.onTriggered = onTriggered; } + /// + /// Gets the priority of this entry. + /// + public ulong Priority { get; init; } + /// /// Gets or sets the name of this entry. /// @@ -88,6 +192,11 @@ namespace Dalamud.Interface /// public TextureWrap Texture { get; set; } + /// + /// Gets the calling assembly of this entry. + /// + internal Assembly? CallingAssembly { get; init; } + /// /// Gets the internal ID of this entry. /// @@ -100,6 +209,32 @@ namespace Dalamud.Interface { this.onTriggered(); } + + /// + public int CompareTo(TitleScreenMenuEntry? other) + { + if (other == null) + return 1; + if (this.CallingAssembly != other.CallingAssembly) + { + if (this.CallingAssembly == null && other.CallingAssembly == null) + return 0; + if (this.CallingAssembly == null && other.CallingAssembly != null) + return -1; + if (this.CallingAssembly != null && other.CallingAssembly == null) + return 1; + return string.Compare( + this.CallingAssembly!.FullName!, + other.CallingAssembly!.FullName!, + StringComparison.CurrentCultureIgnoreCase); + } + + if (this.Priority != other.Priority) + return this.Priority.CompareTo(other.Priority); + if (this.Name != other.Name) + return string.Compare(this.Name, other.Name, StringComparison.InvariantCultureIgnoreCase); + return string.Compare(this.Name, other.Name, StringComparison.InvariantCulture); + } } } } diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index 0119d1a78..3a8927da9 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Diagnostics; - +using System.Threading.Tasks; using Dalamud.Configuration.Internal; +using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.GameFonts; @@ -24,6 +25,8 @@ namespace Dalamud.Interface { private readonly Stopwatch stopwatch; private readonly string namespaceName; + private readonly InterfaceManager interfaceManager = Service.Get(); + private readonly GameFontManager gameFontManager = Service.Get(); private bool hasErrorWindow = false; private bool lastFrameUiHideState = false; @@ -38,11 +41,10 @@ namespace Dalamud.Interface this.stopwatch = new Stopwatch(); this.namespaceName = namespaceName; - var interfaceManager = Service.Get(); - interfaceManager.Draw += this.OnDraw; - interfaceManager.BuildFonts += this.OnBuildFonts; - interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; - interfaceManager.ResizeBuffers += this.OnResizeBuffers; + this.interfaceManager.Draw += this.OnDraw; + this.interfaceManager.BuildFonts += this.OnBuildFonts; + this.interfaceManager.AfterBuildFonts += this.OnAfterBuildFonts; + this.interfaceManager.ResizeBuffers += this.OnResizeBuffers; } /// @@ -109,12 +111,12 @@ namespace Dalamud.Interface /// /// Gets the game's active Direct3D device. /// - public Device Device => Service.Get().Manager.Device!; + public Device Device => this.InterfaceManagerWithScene.Device!; /// /// Gets the game's main window handle. /// - public IntPtr WindowHandlePtr => Service.Get().Manager.WindowHandlePtr; + public IntPtr WindowHandlePtr => this.InterfaceManagerWithScene.WindowHandlePtr; /// /// Gets or sets a value indicating whether this plugin should hide its UI automatically when the game's UI is hidden. @@ -141,8 +143,8 @@ namespace Dalamud.Interface /// public bool OverrideGameCursor { - get => Service.Get().OverrideGameCursor; - set => Service.Get().OverrideGameCursor = value; + get => this.interfaceManager.OverrideGameCursor; + set => this.interfaceManager.OverrideGameCursor = value; } /// @@ -157,7 +159,9 @@ namespace Dalamud.Interface { get { - var condition = Service.Get(); + var condition = Service.GetNullable(); + if (condition == null) + return false; return condition[ConditionFlag.OccupiedInCutSceneEvent] || condition[ConditionFlag.WatchingCutscene78]; } @@ -170,7 +174,9 @@ namespace Dalamud.Interface { get { - var condition = Service.Get(); + var condition = Service.GetNullable(); + if (condition == null) + return false; return condition[ConditionFlag.WatchingCutscene]; } } @@ -178,7 +184,12 @@ namespace Dalamud.Interface /// /// Gets a value indicating whether this plugin should modify the game's interface at this time. /// - public bool ShouldModifyUi => Service.GetNullable()?.IsDispatchingEvents ?? true; + public bool ShouldModifyUi => this.interfaceManager.IsDispatchingEvents; + + /// + /// Gets a value indicating whether UI functions can be used. + /// + public bool UiPrepared => Service.GetNullable() != null; /// /// Gets or sets a value indicating whether statistics about UI draw time should be collected. @@ -209,13 +220,20 @@ namespace Dalamud.Interface /// internal List DrawTimeHistory { get; set; } = new List(); + private InterfaceManager? InterfaceManagerWithScene => + Service.GetNullable()?.Manager; + + private Task InterfaceManagerWithSceneAsync => + Service.GetAsync().ContinueWith(task => task.Result.Manager); + /// /// Loads an image from the specified file. /// /// The full filepath to the image. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImage(string filePath) - => Service.Get().Manager.LoadImage(filePath); + => this.InterfaceManagerWithScene?.LoadImage(filePath) + ?? throw new InvalidOperationException("Load failed."); /// /// Loads an image from a byte stream, such as a png downloaded into memory. @@ -223,7 +241,8 @@ namespace Dalamud.Interface /// A byte array containing the raw image data. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImage(byte[] imageData) - => Service.Get().Manager.LoadImage(imageData); + => this.InterfaceManagerWithScene?.LoadImage(imageData) + ?? throw new InvalidOperationException("Load failed."); /// /// Loads an image from raw unformatted pixel data, with no type or header information. To load formatted data, use . @@ -234,14 +253,99 @@ namespace Dalamud.Interface /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. /// A object wrapping the created image. Use inside ImGui.Image(). public TextureWrap LoadImageRaw(byte[] imageData, int width, int height, int numChannels) - => Service.Get().Manager.LoadImageRaw(imageData, width, height, numChannels); + => this.InterfaceManagerWithScene?.LoadImageRaw(imageData, width, height, numChannels) + ?? throw new InvalidOperationException("Load failed."); + + /// + /// Asynchronously loads an image from the specified file, when it's possible to do so. + /// + /// The full filepath to the image. + /// A object wrapping the created image. Use inside ImGui.Image(). + public Task LoadImageAsync(string filePath) => Task.Run( + async () => + (await this.InterfaceManagerWithSceneAsync).LoadImage(filePath) + ?? throw new InvalidOperationException("Load failed.")); + + /// + /// Asynchronously loads an image from a byte stream, such as a png downloaded into memory, when it's possible to do so. + /// + /// A byte array containing the raw image data. + /// A object wrapping the created image. Use inside ImGui.Image(). + public Task LoadImageAsync(byte[] imageData) => Task.Run( + async () => + (await this.InterfaceManagerWithSceneAsync).LoadImage(imageData) + ?? throw new InvalidOperationException("Load failed.")); + + /// + /// Asynchronously loads an image from raw unformatted pixel data, with no type or header information, when it's possible to do so. To load formatted data, use . + /// + /// A byte array containing the raw pixel data. + /// The width of the image contained in . + /// The height of the image contained in . + /// The number of channels (bytes per pixel) of the image contained in . This should usually be 4. + /// A object wrapping the created image. Use inside ImGui.Image(). + public Task LoadImageRawAsync(byte[] imageData, int width, int height, int numChannels) => Task.Run( + async () => + (await this.InterfaceManagerWithSceneAsync).LoadImageRaw(imageData, width, height, numChannels) + ?? throw new InvalidOperationException("Load failed.")); + + /// + /// Waits for UI to become available for use. + /// + /// A task that completes when the game's Present has been called at least once. + public Task WaitForUi() => this.InterfaceManagerWithSceneAsync; + + /// + /// Waits for UI to become available for use. + /// + /// Function to call. + /// Specifies whether to call the function from the framework thread. + /// A task that completes when the game's Present has been called at least once. + /// Return type. + public Task RunWhenUiPrepared(Func func, bool runInFrameworkThread = false) + { + if (runInFrameworkThread) + { + return this.InterfaceManagerWithSceneAsync + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .Unwrap(); + } + else + { + return this.InterfaceManagerWithSceneAsync + .ContinueWith(_ => func()); + } + } + + /// + /// Waits for UI to become available for use. + /// + /// Function to call. + /// Specifies whether to call the function from the framework thread. + /// A task that completes when the game's Present has been called at least once. + /// Return type. + public Task RunWhenUiPrepared(Func> func, bool runInFrameworkThread = false) + { + if (runInFrameworkThread) + { + return this.InterfaceManagerWithSceneAsync + .ContinueWith(_ => Service.Get().RunOnFrameworkThread(func)) + .Unwrap(); + } + else + { + return this.InterfaceManagerWithSceneAsync + .ContinueWith(_ => func()) + .Unwrap(); + } + } /// /// Gets a game font. /// /// Font to get. /// Handle to the game font which may or may not be available for use yet. - public GameFontHandle GetGameFontHandle(GameFontStyle style) => Service.Get().NewFontRef(style); + public GameFontHandle GetGameFontHandle(GameFontStyle style) => this.gameFontManager.NewFontRef(style); /// /// Call this to queue a rebuild of the font atlas.
@@ -251,7 +355,7 @@ namespace Dalamud.Interface public void RebuildFonts() { Log.Verbose("[FONT] {0} plugin is initiating FONT REBUILD", this.namespaceName); - Service.Get().RebuildFonts(); + this.interfaceManager.RebuildFonts(); } /// @@ -262,19 +366,25 @@ namespace Dalamud.Interface /// The type of the notification. /// The time the notification should be displayed for. public void AddNotification( - string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) => - Service.Get().AddNotification(content, title, type, msDelay); + string content, string? title = null, NotificationType type = NotificationType.None, uint msDelay = 3000) + { + Service + .GetAsync() + .ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + task.Result.AddNotification(content, title, type, msDelay); + }); + } /// /// Unregister the UiBuilder. Do not call this in plugin code. /// void IDisposable.Dispose() { - var interfaceManager = Service.Get(); - - interfaceManager.Draw -= this.OnDraw; - interfaceManager.BuildFonts -= this.OnBuildFonts; - interfaceManager.ResizeBuffers -= this.OnResizeBuffers; + this.interfaceManager.Draw -= this.OnDraw; + this.interfaceManager.BuildFonts -= this.OnBuildFonts; + this.interfaceManager.ResizeBuffers -= this.OnResizeBuffers; } /// @@ -304,8 +414,9 @@ namespace Dalamud.Interface private void OnDraw() { var configuration = Service.Get(); - var gameGui = Service.Get(); - var interfaceManager = Service.Get(); + var gameGui = Service.GetNullable(); + if (gameGui == null) + return; if ((gameGui.GameUiHidden && configuration.ToggleUiHide && !(this.DisableUserUiHide || this.DisableAutomaticUiHide)) || @@ -329,7 +440,7 @@ namespace Dalamud.Interface this.ShowUi?.Invoke(); } - if (!interfaceManager.FontsReady) + if (!this.interfaceManager.FontsReady) return; ImGui.PushID(this.namespaceName); diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index ca7576398..f4a02892e 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -20,6 +20,9 @@ namespace Dalamud.Logging.Internal private static readonly ConcurrentQueue NewlyCreatedTasks = new(); private static bool clearRequested = false; + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private MonoMod.RuntimeDetour.Hook? scheduleAndStartHook; private bool enabled = false; @@ -111,8 +114,7 @@ namespace Dalamud.Logging.Internal this.ApplyPatch(); - var framework = Service.Get(); - framework.Update += this.FrameworkOnUpdate; + this.framework.Update += this.FrameworkOnUpdate; this.enabled = true; } @@ -121,8 +123,7 @@ namespace Dalamud.Logging.Internal { this.scheduleAndStartHook?.Dispose(); - var framework = Service.Get(); - framework.Update -= this.FrameworkOnUpdate; + this.framework.Update -= this.FrameworkOnUpdate; } private static bool AddToActiveTasksHook(Func orig, Task self) diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index e0fa641cc..2a04cd2f6 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -15,6 +15,7 @@ using Dalamud.Game.Text.Sanitizer; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Interface; +using Dalamud.Interface.GameFonts; using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Plugin.Ipc; diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index 9f803f79a..b0c206dd1 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -38,6 +38,11 @@ internal partial class PluginManager : IDisposable, IServiceType /// public const int DalamudApiLevel = 6; + /// + /// Default time to wait between plugin unload and plugin assembly unload. + /// + public const int PluginWaitBeforeFreeDefault = 500; + private static readonly ModuleLog Log = new("PLUGINM"); private readonly object pluginListLock = new(); @@ -207,16 +212,43 @@ internal partial class PluginManager : IDisposable, IServiceType /// public void Dispose() { - foreach (var plugin in this.InstalledPlugins) + if (this.InstalledPlugins.Any()) { - try + // Unload them first, just in case some of plugin codes are still running via callbacks initiated externally. + foreach (var plugin in this.InstalledPlugins.Where(plugin => !plugin.Manifest.CanUnloadAsync)) { - plugin.Dispose(); - } - catch (Exception ex) - { - Log.Error(ex, $"Error disposing {plugin.Name}"); + try + { + plugin.UnloadAsync(true, false).Wait(); + } + catch (Exception ex) + { + Log.Error(ex, $"Error unloading {plugin.Name}"); + } } + + Task.WaitAll(this.InstalledPlugins + .Where(plugin => plugin.Manifest.CanUnloadAsync) + .Select(plugin => Task.Run(async () => + { + try + { + await plugin.UnloadAsync(true, false); + } + catch (Exception ex) + { + Log.Error(ex, $"Error unloading {plugin.Name}"); + } + })).ToArray()); + + // Just in case plugins still have tasks running that they didn't cancel when they should have, + // give them some time to complete it. + Thread.Sleep(this.configuration.PluginWaitBeforeFree ?? PluginWaitBeforeFreeDefault); + + // Now that we've waited enough, dispose the whole plugin. + // Since plugins should have been unloaded above, this should be done quickly. + foreach (var plugin in this.InstalledPlugins) + plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log); } this.assemblyLocationMonoHook?.Dispose(); @@ -891,7 +923,7 @@ internal partial class PluginManager : IDisposable, IServiceType { try { - plugin.Unload(); + await plugin.UnloadAsync(); } catch (Exception ex) { @@ -963,23 +995,30 @@ internal partial class PluginManager : IDisposable, IServiceType /// /// The plugin. /// Throws if the plugin is still loading/unloading. - public void DeleteConfiguration(LocalPlugin plugin) + /// The task. + public async Task DeleteConfigurationAsync(LocalPlugin plugin) { - if (plugin.State == PluginState.InProgress) + if (plugin.State == PluginState.Loading || plugin.State == PluginState.Unloaded) throw new Exception("Cannot delete configuration for a loading/unloading plugin"); if (plugin.IsLoaded) - plugin.Unload(); + await plugin.UnloadAsync(); - // Let's wait so any handles on files in plugin configurations can be closed - Thread.Sleep(500); - - this.PluginConfigs.Delete(plugin.Name); - - Thread.Sleep(500); + for (var waitUntil = Environment.TickCount64 + 1000; Environment.TickCount64 < waitUntil;) + { + try + { + this.PluginConfigs.Delete(plugin.Name); + break; + } + catch (IOException) + { + await Task.Delay(100); + } + } // Let's indicate "installer" here since this is supposed to be a fresh install - plugin.LoadAsync(PluginLoadReason.Installer).Wait(); + await plugin.LoadAsync(PluginLoadReason.Installer); } /// diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index e10081d3a..ec90d9c3f 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -2,10 +2,13 @@ using System; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; using Dalamud.Game; using Dalamud.Game.Gui.Dtr; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Exceptions; @@ -27,6 +30,8 @@ internal class LocalPlugin : IDisposable private readonly FileInfo disabledFile; private readonly FileInfo testingFile; + private readonly SemaphoreSlim pluginLoadStateLock = new(1); + private PluginLoader? loader; private Assembly? pluginAssembly; private Type? pluginType; @@ -208,8 +213,20 @@ internal class LocalPlugin : IDisposable /// public void Dispose() { - this.instance?.Dispose(); - this.instance = null; + var framework = Service.GetNullable(); + var configuration = Service.Get(); + + var didPluginDispose = false; + if (this.instance != null) + { + didPluginDispose = true; + if (this.Manifest.CanUnloadAsync || framework == null) + this.instance.Dispose(); + else + framework.RunOnFrameworkThread(() => this.instance.Dispose()).Wait(); + + this.instance = null; + } this.DalamudInterface?.ExplicitDispose(); this.DalamudInterface = null; @@ -217,6 +234,8 @@ internal class LocalPlugin : IDisposable this.pluginType = null; this.pluginAssembly = null; + if (this.loader != null && didPluginDispose) + Thread.Sleep(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault); this.loader?.Dispose(); } @@ -228,54 +247,73 @@ internal class LocalPlugin : IDisposable /// A task. public async Task LoadAsync(PluginLoadReason reason, bool reloading = false) { - var startInfo = Service.Get(); - var configuration = Service.Get(); - var pluginManager = Service.Get(); + var configuration = await Service.GetAsync(); + var framework = await Service.GetAsync(); + var ioc = await Service.GetAsync(); + var pluginManager = await Service.GetAsync(); + var startInfo = await Service.GetAsync(); - // Allowed: Unloaded - switch (this.State) - { - case PluginState.InProgress: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, already working"); - case PluginState.Loaded: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded"); - case PluginState.LoadError: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, load previously faulted, unload first"); - case PluginState.UnloadError: - throw new InvalidPluginOperationException($"Unable to load {this.Name}, unload previously faulted, restart Dalamud"); - case PluginState.Unloaded: - break; - default: - throw new ArgumentOutOfRangeException(this.State.ToString()); - } + // UiBuilder constructor requires the following two. + await Service.GetAsync(); + await Service.GetAsync(); - if (pluginManager.IsManifestBanned(this.Manifest)) - throw new BannedPluginException($"Unable to load {this.Name}, banned"); - - if (this.Manifest.ApplicableVersion < startInfo.GameVersion) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); - - if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); - - if (this.Manifest.Disabled) - throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled"); - - this.State = PluginState.InProgress; - Log.Information($"Loading {this.DllFile.Name}"); - - if (this.DllFile.DirectoryName != null && File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll"))) - { - Log.Error("==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", this.Manifest.Author!, this.Manifest.InternalName); - Log.Error("YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!"); - Log.Error("You may not be able to load your plugin. \"False\" needs to be set in your csproj."); - Log.Error("If you are using ILMerge, do not merge anything other than your direct dependencies."); - Log.Error("Do not merge FFXIVClientStructs.Generators.dll."); - Log.Error("Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information."); - } + if (this.Manifest.LoadRequiredState == 0) + _ = await Service.GetAsync(); + await this.pluginLoadStateLock.WaitAsync(); try { + switch (this.State) + { + case PluginState.Loaded: + throw new InvalidPluginOperationException($"Unable to load {this.Name}, already loaded"); + case PluginState.LoadError: + throw new InvalidPluginOperationException( + $"Unable to load {this.Name}, load previously faulted, unload first"); + case PluginState.UnloadError: + throw new InvalidPluginOperationException( + $"Unable to load {this.Name}, unload previously faulted, restart Dalamud"); + case PluginState.Unloaded: + break; + case PluginState.Loading: + case PluginState.Unloading: + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + if (pluginManager.IsManifestBanned(this.Manifest)) + throw new BannedPluginException($"Unable to load {this.Name}, banned"); + + if (this.Manifest.ApplicableVersion < startInfo.GameVersion) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, no applicable version"); + + if (this.Manifest.DalamudApiLevel < PluginManager.DalamudApiLevel && !configuration.LoadAllApiLevels) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, incompatible API level"); + + if (this.Manifest.Disabled) + throw new InvalidPluginOperationException($"Unable to load {this.Name}, disabled"); + + this.State = PluginState.Loading; + Log.Information($"Loading {this.DllFile.Name}"); + + if (this.DllFile.DirectoryName != null && + File.Exists(Path.Combine(this.DllFile.DirectoryName, "Dalamud.dll"))) + { + Log.Error( + "==== IMPORTANT MESSAGE TO {0}, THE DEVELOPER OF {1} ====", + this.Manifest.Author!, + this.Manifest.InternalName); + Log.Error( + "YOU ARE INCLUDING DALAMUD DEPENDENCIES IN YOUR BUILDS!!!"); + Log.Error( + "You may not be able to load your plugin. \"False\" needs to be set in your csproj."); + Log.Error( + "If you are using ILMerge, do not merge anything other than your direct dependencies."); + Log.Error("Do not merge FFXIVClientStructs.Generators.dll."); + Log.Error( + "Please refer to https://github.com/goatcorp/Dalamud/discussions/603 for more information."); + } + this.loader ??= PluginLoader.CreateFromAssemblyFile(this.DllFile.FullName, SetupLoaderConfig); if (reloading || this.IsDev) @@ -309,7 +347,8 @@ internal class LocalPlugin : IDisposable this.AssemblyName = this.pluginAssembly.GetName(); // Find the plugin interface implementation. It is guaranteed to exist after checking in the ctor. - this.pluginType ??= this.pluginAssembly.GetTypes().First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); + this.pluginType ??= this.pluginAssembly.GetTypes() + .First(type => type.IsAssignableTo(typeof(IDalamudPlugin))); // Check for any loaded plugins with the same assembly name var assemblyName = this.pluginAssembly.GetName().Name; @@ -319,7 +358,8 @@ internal class LocalPlugin : IDisposable if (otherPlugin == this || otherPlugin.instance == null) continue; - var otherPluginAssemblyName = otherPlugin.instance.GetType().Assembly.GetName().Name; + var otherPluginAssemblyName = + otherPlugin.instance.GetType().Assembly.GetName().Name; if (otherPluginAssemblyName == assemblyName && otherPluginAssemblyName != null) { this.State = PluginState.Unloaded; @@ -330,17 +370,29 @@ internal class LocalPlugin : IDisposable } // Update the location for the Location and CodeBase patches - PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = new PluginPatchData(this.DllFile); + PluginManager.PluginLocations[this.pluginType.Assembly.FullName] = + new PluginPatchData(this.DllFile); - this.DalamudInterface = new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev); + this.DalamudInterface = + new DalamudPluginInterface(this.pluginAssembly.GetName().Name!, this.DllFile, reason, this.IsDev); + + if (this.Manifest.LoadSync && this.Manifest.LoadRequiredState is 0 or 1) + { + this.instance = await framework.RunOnFrameworkThread( + () => ioc.CreateAsync(this.pluginType!, this.DalamudInterface!)) as IDalamudPlugin; + } + else + { + this.instance = + await ioc.CreateAsync(this.pluginType!, this.DalamudInterface!) as IDalamudPlugin; + } - var ioc = Service.Get(); - this.instance = await ioc.CreateAsync(this.pluginType, this.DalamudInterface) as IDalamudPlugin; if (this.instance == null) { this.State = PluginState.LoadError; this.DalamudInterface.ExplicitDispose(); - Log.Error($"Error while loading {this.Name}, failed to bind and call the plugin constructor"); + Log.Error( + $"Error while loading {this.Name}, failed to bind and call the plugin constructor"); return; } @@ -363,6 +415,10 @@ internal class LocalPlugin : IDisposable throw; } + finally + { + this.pluginLoadStateLock.Release(); + } } /// @@ -370,31 +426,40 @@ internal class LocalPlugin : IDisposable /// in the plugin list until it has been actually disposed. /// /// Unload while reloading. - public void Unload(bool reloading = false) + /// Wait before disposing loader. + /// The task. + public async Task UnloadAsync(bool reloading = false, bool waitBeforeLoaderDispose = true) { - // Allowed: Loaded, LoadError(we are cleaning this up while we're at it) - switch (this.State) - { - case PluginState.InProgress: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already working"); - case PluginState.Unloaded: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); - case PluginState.UnloadError: - throw new InvalidPluginOperationException($"Unable to unload {this.Name}, unload previously faulted, restart Dalamud"); - case PluginState.Loaded: - break; - case PluginState.LoadError: - break; - default: - throw new ArgumentOutOfRangeException(this.State.ToString()); - } + var configuration = Service.Get(); + var framework = Service.GetNullable(); + await this.pluginLoadStateLock.WaitAsync(); try { - this.State = PluginState.InProgress; + switch (this.State) + { + case PluginState.Unloaded: + throw new InvalidPluginOperationException($"Unable to unload {this.Name}, already unloaded"); + case PluginState.UnloadError: + throw new InvalidPluginOperationException( + $"Unable to unload {this.Name}, unload previously faulted, restart Dalamud"); + case PluginState.Loaded: + case PluginState.LoadError: + break; + case PluginState.Loading: + case PluginState.Unloading: + default: + throw new ArgumentOutOfRangeException(this.State.ToString()); + } + + this.State = PluginState.Unloading; Log.Information($"Unloading {this.DllFile.Name}"); - this.instance?.Dispose(); + if (this.Manifest.CanUnloadAsync || framework == null) + this.instance?.Dispose(); + else + await framework.RunOnFrameworkThread(() => this.instance?.Dispose()); + this.instance = null; this.DalamudInterface?.ExplicitDispose(); @@ -405,6 +470,8 @@ internal class LocalPlugin : IDisposable if (!reloading) { + if (waitBeforeLoaderDispose && this.loader != null) + await Task.Delay(configuration.PluginWaitBeforeFree ?? PluginManager.PluginWaitBeforeFreeDefault); this.loader?.Dispose(); this.loader = null; } @@ -419,6 +486,13 @@ internal class LocalPlugin : IDisposable throw; } + finally + { + // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. + Service.GetNullable()?.HandleRemovedNodes(); + + this.pluginLoadStateLock.Release(); + } } /// @@ -427,12 +501,7 @@ internal class LocalPlugin : IDisposable /// A task. public async Task ReloadAsync() { - this.Unload(true); - - // We need to handle removed DTR nodes here, as otherwise, plugins will not be able to re-add their bar entries after updates. - var dtr = Service.Get(); - dtr.HandleRemovedNodes(); - + await this.UnloadAsync(true); await this.LoadAsync(PluginLoadReason.Reload, true); } @@ -444,7 +513,8 @@ internal class LocalPlugin : IDisposable // Allowed: Unloaded, UnloadError switch (this.State) { - case PluginState.InProgress: + case PluginState.Loading: + case PluginState.Unloading: case PluginState.Loaded: case PluginState.LoadError: throw new InvalidPluginOperationException($"Unable to enable {this.Name}, still loaded"); @@ -471,7 +541,8 @@ internal class LocalPlugin : IDisposable // Allowed: Unloaded, UnloadError switch (this.State) { - case PluginState.InProgress: + case PluginState.Loading: + case PluginState.Unloading: case PluginState.Loaded: case PluginState.LoadError: throw new InvalidPluginOperationException($"Unable to disable {this.Name}, still loaded"); diff --git a/Dalamud/Plugin/Internal/Types/PluginManifest.cs b/Dalamud/Plugin/Internal/Types/PluginManifest.cs index eaff9e77f..c4d712a90 100644 --- a/Dalamud/Plugin/Internal/Types/PluginManifest.cs +++ b/Dalamud/Plugin/Internal/Types/PluginManifest.cs @@ -156,6 +156,12 @@ internal record PluginManifest [JsonProperty] public int LoadPriority { get; init; } + /// + /// Gets a value indicating whether the plugin can be unloaded asynchronously. + /// + [JsonProperty] + public bool CanUnloadAsync { get; init; } + /// /// Gets a list of screenshot image URLs to show in the plugin installer. /// diff --git a/Dalamud/Plugin/Internal/Types/PluginState.cs b/Dalamud/Plugin/Internal/Types/PluginState.cs index da5fcf977..7a8074801 100644 --- a/Dalamud/Plugin/Internal/Types/PluginState.cs +++ b/Dalamud/Plugin/Internal/Types/PluginState.cs @@ -16,9 +16,9 @@ internal enum PluginState UnloadError, /// - /// Currently loading. + /// Currently unloading. /// - InProgress, + Unloading, /// /// Load is successful. @@ -29,4 +29,9 @@ internal enum PluginState /// Plugin has thrown an error during loading. /// LoadError, + + /// + /// Currently loading. + /// + Loading, } diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index 80dd01a90..738b80a19 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -25,6 +25,8 @@ namespace Dalamud private static readonly TaskCompletionSource BlockingServicesLoadedTaskCompletionSource = new(); + private static readonly List LoadedServices = new(); + /// /// Gets task that gets completed when all blocking early loading services are done loading. /// @@ -38,20 +40,35 @@ namespace Dalamud /// Instance of . public static void InitializeProvidedServicesAndClientStructs(Dalamud dalamud, DalamudStartInfo startInfo, DalamudConfiguration configuration) { - Service.Provide(dalamud); - Service.Provide(startInfo); - Service.Provide(configuration); - Service.Provide(new ServiceContainer()); - // Initialize the process information. var cacheDir = new DirectoryInfo(Path.Combine(startInfo.WorkingDirectory!, "cachedSigs")); if (!cacheDir.Exists) cacheDir.Create(); - Service.Provide(new SigScanner(true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); + + lock (LoadedServices) + { + Service.Provide(dalamud); + LoadedServices.Add(typeof(Dalamud)); + + Service.Provide(startInfo); + LoadedServices.Add(typeof(DalamudStartInfo)); + + Service.Provide(configuration); + LoadedServices.Add(typeof(DalamudConfiguration)); + + Service.Provide(new ServiceContainer()); + LoadedServices.Add(typeof(ServiceContainer)); + + Service.Provide( + new SigScanner( + true, new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}.json")))); + LoadedServices.Add(typeof(SigScanner)); + } using (Timings.Start("CS Resolver Init")) { - FFXIVClientStructs.Resolver.InitializeParallel(new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); + FFXIVClientStructs.Resolver.InitializeParallel( + new FileInfo(Path.Combine(cacheDir.FullName, $"{startInfo.GameVersion}_cs.json"))); } } @@ -62,7 +79,6 @@ namespace Dalamud public static async Task InitializeEarlyLoadableServices() { using var serviceInitializeTimings = Timings.Start("Services Init"); - var service = typeof(Service<>); var earlyLoadingServices = new HashSet(); var blockingEarlyLoadingServices = new HashSet(); @@ -76,12 +92,14 @@ namespace Dalamud if (attr?.IsAssignableTo(typeof(EarlyLoadedService)) != true) continue; - var getTask = (Task)service.MakeGenericType(serviceType).InvokeMember( - "GetAsync", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null); + var getTask = (Task)typeof(Service<>) + .MakeGenericType(serviceType) + .InvokeMember( + "GetAsync", + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, + null, + null, + null); if (attr.IsAssignableTo(typeof(BlockingEarlyLoadedService))) { @@ -94,7 +112,7 @@ namespace Dalamud } dependencyServicesMap[serviceType] = - (List)service + (List)typeof(Service<>) .MakeGenericType(serviceType) .InvokeMember( "GetDependencyServices", @@ -118,9 +136,9 @@ namespace Dalamud } }).ConfigureAwait(false); + var tasks = new List(); try { - var tasks = new List(); var servicesToLoad = new HashSet(); servicesToLoad.UnionWith(earlyLoadingServices); servicesToLoad.UnionWith(blockingEarlyLoadingServices); @@ -133,13 +151,25 @@ namespace Dalamud x => getAsyncTaskMap.GetValueOrDefault(x)?.IsCompleted == false)) continue; - tasks.Add((Task)service.MakeGenericType(serviceType).InvokeMember( - "StartLoader", - BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.Public, - null, - null, - null)); + tasks.Add((Task)typeof(Service<>) + .MakeGenericType(serviceType) + .InvokeMember( + "StartLoader", + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, + null, + null, + null)); servicesToLoad.Remove(serviceType); + + tasks.Add(tasks.Last().ContinueWith(task => + { + if (task.IsFaulted) + return; + lock (LoadedServices) + { + LoadedServices.Add(serviceType); + } + })); } if (!tasks.Any()) @@ -172,10 +202,49 @@ namespace Dalamud // don't care, as this means task result/exception has already been set } + while (tasks.Any()) + { + await Task.WhenAny(tasks); + tasks.RemoveAll(x => x.IsCompleted); + } + + UnloadAllServices(); + throw; } } + /// + /// Unloads all services, in the reverse order of load. + /// + public static void UnloadAllServices() + { + var framework = Service.GetNullable(Service.ExceptionPropagationMode.None); + if (framework is { IsInFrameworkUpdateThread: false, IsFrameworkUnloading: false }) + { + framework.RunOnFrameworkThread(UnloadAllServices).Wait(); + return; + } + + lock (LoadedServices) + { + while (LoadedServices.Any()) + { + var serviceType = LoadedServices.Last(); + LoadedServices.RemoveAt(LoadedServices.Count - 1); + + typeof(Service<>) + .MakeGenericType(serviceType) + .InvokeMember( + "Unset", + BindingFlags.InvokeMethod | BindingFlags.Static | BindingFlags.NonPublic, + null, + null, + null); + } + } + } + /// /// Indicates that this constructor will be called for early initialization. /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index f813ef8dd..72751316d 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -19,8 +19,7 @@ namespace Dalamud /// The class you want to store in the service locator. internal static class Service where T : IServiceType { - // ReSharper disable once StaticMemberInGenericType - private static readonly TaskCompletionSource InstanceTcs = new(); + private static TaskCompletionSource instanceTcs = new(); static Service() { @@ -31,50 +30,28 @@ namespace Dalamud ServiceManager.Log.Debug("Service<{0}>: Static ctor called", typeof(T).Name); if (exposeToPlugins) - Service.Get().RegisterSingleton(InstanceTcs.Task); + Service.Get().RegisterSingleton(instanceTcs.Task); } /// - /// Initializes the service. + /// Specifies how to handle the cases of failed services when calling . /// - /// The object. - [UsedImplicitly] - public static Task StartLoader() + public enum ExceptionPropagationMode { - var attr = typeof(T).GetCustomAttribute(true)?.GetType(); - if (attr?.IsAssignableTo(typeof(ServiceManager.EarlyLoadedService)) != true) - throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); + /// + /// Propagate all exceptions. + /// + PropagateAll, - return Task.Run(Timings.AttachTimingHandle(async () => - { - ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); - try - { - var instance = await ConstructObject(); - InstanceTcs.SetResult(instance); + /// + /// Propagate all exceptions, except for . + /// + PropagateNonUnloaded, - foreach (var method in typeof(T).GetMethods( - BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) - { - if (method.GetCustomAttribute(true) == null) - continue; - - 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); - } - - ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name); - return instance; - } - catch (Exception e) - { - ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name); - InstanceTcs.SetException(e); - throw; - } - })); + /// + /// Treat all exceptions as null. + /// + None, } /// @@ -83,7 +60,7 @@ namespace Dalamud /// Object to set. public static void Provide(T obj) { - InstanceTcs.SetResult(obj); + instanceTcs.SetResult(obj); ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); } @@ -94,7 +71,7 @@ namespace Dalamud public static void ProvideException(Exception exception) { ServiceManager.Log.Error(exception, "Service<{0}>: Error", typeof(T).Name); - InstanceTcs.SetException(exception); + instanceTcs.SetException(exception); } /// @@ -103,9 +80,9 @@ namespace Dalamud /// The object. public static T Get() { - if (!InstanceTcs.Task.IsCompleted) - InstanceTcs.Task.Wait(); - return InstanceTcs.Task.Result; + if (!instanceTcs.Task.IsCompleted) + instanceTcs.Task.Wait(); + return instanceTcs.Task.Result; } /// @@ -113,13 +90,27 @@ namespace Dalamud /// /// The object. [UsedImplicitly] - public static Task GetAsync() => InstanceTcs.Task; + public static Task GetAsync() => instanceTcs.Task; /// /// Attempt to pull the instance out of the service locator. /// + /// Specifies which exceptions to propagate. /// The object if registered, null otherwise. - public static T? GetNullable() => InstanceTcs.Task.IsCompleted ? InstanceTcs.Task.Result : default; + public static T? GetNullable(ExceptionPropagationMode propagateException = ExceptionPropagationMode.PropagateNonUnloaded) + { + if (instanceTcs.Task.IsCompletedSuccessfully) + return instanceTcs.Task.Result; + if (instanceTcs.Task.IsFaulted && propagateException != ExceptionPropagationMode.None) + { + if (propagateException == ExceptionPropagationMode.PropagateNonUnloaded + && instanceTcs.Task.Exception!.InnerExceptions.FirstOrDefault() is UnloadedException) + return default; + throw instanceTcs.Task.Exception!; + } + + return default; + } /// /// Gets an enumerable containing Service<T>s that are required for this Service to initialize without blocking. @@ -142,6 +133,77 @@ namespace Dalamud .ToList(); } + [UsedImplicitly] + private 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) + throw new InvalidOperationException($"{typeof(T).Name} is not an EarlyLoadedService"); + + return Task.Run(Timings.AttachTimingHandle(async () => + { + ServiceManager.Log.Debug("Service<{0}>: Begin construction", typeof(T).Name); + try + { + var instance = await ConstructObject(); + instanceTcs.SetResult(instance); + + foreach (var method in typeof(T).GetMethods( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) + { + if (method.GetCustomAttribute(true) == null) + continue; + + 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); + } + + ServiceManager.Log.Debug("Service<{0}>: Construction complete", typeof(T).Name); + return instance; + } + catch (Exception e) + { + ServiceManager.Log.Error(e, "Service<{0}>: Construction failure", typeof(T).Name); + instanceTcs.SetException(e); + throw; + } + })); + } + + [UsedImplicitly] + private static void Unset() + { + if (!instanceTcs.Task.IsCompletedSuccessfully) + return; + + var instance = instanceTcs.Task.Result; + if (instance is IDisposable disposable) + { + ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); + try + { + disposable.Dispose(); + ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); + } + catch (Exception e) + { + ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); + } + } + else + { + ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + } + + instanceTcs = new TaskCompletionSource(); + instanceTcs.SetException(new UnloadedException()); + } + private static async Task ResolveServiceFromTypeAsync(Type type) { var task = (Task)typeof(Service<>) @@ -180,5 +242,19 @@ namespace Dalamud return (T)ctor.Invoke(args)!; } } + + /// + /// Exception thrown when service is attempted to be retrieved when it's unloaded. + /// + public class UnloadedException : InvalidOperationException + { + /// + /// Initializes a new instance of the class. + /// + public UnloadedException() + : base("Service is unloaded.") + { + } + } } } diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 9c02efe2c..ab34b47f5 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -15,6 +15,7 @@ using Dalamud.Game; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface; using Dalamud.Interface.Colors; +using Dalamud.Logging.Internal; using ImGuiNET; using Microsoft.Win32; using Serilog; @@ -536,6 +537,31 @@ namespace Dalamud.Utility obj.Dispose(); } + /// + /// Dispose this object. + /// + /// The object to dispose. + /// Log message to print, if specified and an error occurs. + /// Module logger, if any. + /// The type of object to dispose. + internal static void ExplicitDisposeIgnoreExceptions(this T obj, string? logMessage = null, ModuleLog? moduleLog = null) where T : IDisposable + { + try + { + obj.Dispose(); + } + catch (Exception e) + { + if (logMessage == null) + return; + + if (moduleLog != null) + moduleLog.Error(e, logMessage); + else + Log.Error(e, logMessage); + } + } + private static unsafe void ShowValue(ulong addr, IEnumerable path, Type type, object value) { if (type.IsPointer)