diff --git a/Dalamud.CorePlugin/PluginImpl.cs b/Dalamud.CorePlugin/PluginImpl.cs index cb9b4368a..7c9adc6a8 100644 --- a/Dalamud.CorePlugin/PluginImpl.cs +++ b/Dalamud.CorePlugin/PluginImpl.cs @@ -97,8 +97,6 @@ namespace Dalamud.CorePlugin this.Interface.UiBuilder.Draw -= this.OnDraw; this.windowSystem.RemoveAllWindows(); - - this.Interface.ExplicitDispose(); } /// diff --git a/Dalamud/Configuration/Internal/DalamudConfiguration.cs b/Dalamud/Configuration/Internal/DalamudConfiguration.cs index 85a9507c9..70ed5dfde 100644 --- a/Dalamud/Configuration/Internal/DalamudConfiguration.cs +++ b/Dalamud/Configuration/Internal/DalamudConfiguration.cs @@ -26,7 +26,7 @@ namespace Dalamud.Configuration.Internal; #pragma warning disable SA1015 [InherentDependency] // We must still have this when unloading #pragma warning restore SA1015 -internal sealed class DalamudConfiguration : IServiceType, IDisposable +internal sealed class DalamudConfiguration : IInternalDisposableService { private static readonly JsonSerializerSettings SerializerSettings = new() { @@ -502,7 +502,7 @@ internal sealed class DalamudConfiguration : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // Make sure that we save, if a save is queued while we are shutting down this.Update(); diff --git a/Dalamud/Dalamud.cs b/Dalamud/Dalamud.cs index 8c858ce7c..f9d2aff3c 100644 --- a/Dalamud/Dalamud.cs +++ b/Dalamud/Dalamud.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using Dalamud.Common; using Dalamud.Configuration.Internal; using Dalamud.Game; -using Dalamud.Interface.Internal; using Dalamud.Plugin.Internal; using Dalamud.Storage; using Dalamud.Utility; @@ -187,27 +186,6 @@ internal sealed class Dalamud : IServiceType this.unloadSignal.WaitOne(); } - /// - /// Dispose subsystems related to plugin handling. - /// - public void DisposePlugins() - { - // this must be done before unloading interface manager, in order to do rebuild - // the correct cascaded WndProc (IME -> RawDX11Scene -> Game). Otherwise the game - // will not receive any windows messages - Service.GetNullable()?.Dispose(); - - // this must be done before unloading plugins, or it can cause a race condition - // due to rendering happening on another thread, where a plugin might receive - // a render call after it has been disposed, which can crash if it attempts to - // use any resources that it freed in its own Dispose method - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - - Service.GetNullable()?.Dispose(); - } - /// /// Replace the current exception handler with the default one. /// diff --git a/Dalamud/Data/DataManager.cs b/Dalamud/Data/DataManager.cs index b08c6ffe7..da93f57c4 100644 --- a/Dalamud/Data/DataManager.cs +++ b/Dalamud/Data/DataManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Data; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DataManager : IDisposable, IServiceType, IDataManager +internal sealed class DataManager : IInternalDisposableService, IDataManager { private readonly Thread luminaResourceThread; private readonly CancellationTokenSource luminaCancellationTokenSource; @@ -158,7 +158,7 @@ internal sealed class DataManager : IDisposable, IServiceType, IDataManager #endregion /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.luminaCancellationTokenSource.Cancel(); } diff --git a/Dalamud/EntryPoint.cs b/Dalamud/EntryPoint.cs index d0f9e8845..1ad3ad8a9 100644 --- a/Dalamud/EntryPoint.cs +++ b/Dalamud/EntryPoint.cs @@ -138,7 +138,9 @@ public sealed class EntryPoint SerilogEventSink.Instance.LogLine += SerilogOnLogLine; // Load configuration first to get some early persistent state, like log level +#pragma warning disable CS0618 // Type or member is obsolete var fs = new ReliableFileStorage(Path.GetDirectoryName(info.ConfigurationPath)!); +#pragma warning restore CS0618 // Type or member is obsolete var configuration = DalamudConfiguration.Load(info.ConfigurationPath!, fs); // Set the appropriate logging level from the configuration diff --git a/Dalamud/Game/Addon/Events/AddonEventManager.cs b/Dalamud/Game/Addon/Events/AddonEventManager.cs index 8ee09bed8..a9b9ef5fa 100644 --- a/Dalamud/Game/Addon/Events/AddonEventManager.cs +++ b/Dalamud/Game/Addon/Events/AddonEventManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Events; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonEventManager : IDisposable, IServiceType +internal unsafe class AddonEventManager : IInternalDisposableService { /// /// PluginName for Dalamud Internal use. @@ -62,7 +62,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType private delegate nint UpdateCursorDelegate(RaptureAtkModule* module); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onUpdateCursor.Dispose(); @@ -204,7 +204,7 @@ internal unsafe class AddonEventManager : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddonEventManager +internal class AddonEventManagerPluginScoped : IInternalDisposableService, IAddonEventManager { [ServiceManager.ServiceDependency] private readonly AddonEventManager eventManagerService = Service.Get(); @@ -225,7 +225,7 @@ internal class AddonEventManagerPluginScoped : IDisposable, IServiceType, IAddon } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // if multiple plugins force cursors and dispose without un-forcing them then all forces will be cleared. if (this.isForcingCursor) diff --git a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs index 37f12ce3a..eefb3b5e9 100644 --- a/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs +++ b/Dalamud/Game/Addon/Lifecycle/AddonLifecycle.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Addon.Lifecycle; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal unsafe class AddonLifecycle : IDisposable, IServiceType +internal unsafe class AddonLifecycle : IInternalDisposableService { private static readonly ModuleLog Log = new("AddonLifecycle"); @@ -89,7 +89,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType internal List EventListeners { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.onAddonSetupHook.Dispose(); this.onAddonSetup2Hook.Dispose(); @@ -383,7 +383,7 @@ internal unsafe class AddonLifecycle : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLifecycle +internal class AddonLifecyclePluginScoped : IInternalDisposableService, IAddonLifecycle { [ServiceManager.ServiceDependency] private readonly AddonLifecycle addonLifecycleService = Service.Get(); @@ -391,7 +391,7 @@ internal class AddonLifecyclePluginScoped : IDisposable, IServiceType, IAddonLif private readonly List eventListeners = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var listener in this.eventListeners) { diff --git a/Dalamud/Game/ChatHandlers.cs b/Dalamud/Game/ChatHandlers.cs index 836fb5ec8..5dd6ed3ba 100644 --- a/Dalamud/Game/ChatHandlers.cs +++ b/Dalamud/Game/ChatHandlers.cs @@ -11,6 +11,7 @@ using Dalamud.Game.Gui; using Dalamud.Game.Text; using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling.Payloads; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Internal.Windows; diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index d387c2e2d..bd4259f5a 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game.ClientState; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class ClientState : IDisposable, IServiceType, IClientState +internal sealed class ClientState : IInternalDisposableService, IClientState { private static readonly ModuleLog Log = new("ClientState"); @@ -115,7 +115,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setupTerritoryTypeHook.Dispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent; @@ -196,7 +196,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState +internal class ClientStatePluginScoped : IInternalDisposableService, IClientState { [ServiceManager.ServiceDependency] private readonly ClientState clientStateService = Service.Get(); @@ -257,7 +257,7 @@ internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState public bool IsGPosing => this.clientStateService.IsGPosing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; this.clientStateService.Login -= this.LoginForward; diff --git a/Dalamud/Game/ClientState/Conditions/Condition.cs b/Dalamud/Game/ClientState/Conditions/Condition.cs index a298b1502..dc8b28494 100644 --- a/Dalamud/Game/ClientState/Conditions/Condition.cs +++ b/Dalamud/Game/ClientState/Conditions/Condition.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.ClientState.Conditions; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class Condition : IServiceType, ICondition +internal sealed partial class Condition : IInternalDisposableService, ICondition { /// /// Gets the current max number of conditions. You can get this just by looking at the condition sheet and how many rows it has. @@ -22,6 +22,8 @@ internal sealed partial class Condition : IServiceType, ICondition private readonly bool[] cache = new bool[MaxConditionEntries]; + private bool isDisposed; + [ServiceManager.ServiceConstructor] private Condition(ClientState clientState) { @@ -35,6 +37,9 @@ internal sealed partial class Condition : IServiceType, ICondition this.framework.Update += this.FrameworkUpdate; } + /// Finalizes an instance of the class. + ~Condition() => this.Dispose(false); + /// public event ICondition.ConditionChangeDelegate? ConditionChange; @@ -60,6 +65,9 @@ internal sealed partial class Condition : IServiceType, ICondition public bool this[ConditionFlag flag] => this[(int)flag]; + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + /// public bool Any() { @@ -89,6 +97,19 @@ internal sealed partial class Condition : IServiceType, ICondition return false; } + private void Dispose(bool disposing) + { + if (this.isDisposed) + return; + + if (disposing) + { + this.framework.Update -= this.FrameworkUpdate; + } + + this.isDisposed = true; + } + private void FrameworkUpdate(IFramework unused) { for (var i = 0; i < MaxConditionEntries; i++) @@ -112,44 +133,6 @@ internal sealed partial class Condition : IServiceType, ICondition } } -/// -/// Provides access to conditions (generally player state). You can check whether a player is in combat, mounted, etc. -/// -internal sealed partial class Condition : IDisposable -{ - private bool isDisposed; - - /// - /// Finalizes an instance of the class. - /// - ~Condition() - { - this.Dispose(false); - } - - /// - /// Disposes this instance, alongside its hooks. - /// - void IDisposable.Dispose() - { - GC.SuppressFinalize(this); - this.Dispose(true); - } - - private void Dispose(bool disposing) - { - if (this.isDisposed) - return; - - if (disposing) - { - this.framework.Update -= this.FrameworkUpdate; - } - - this.isDisposed = true; - } -} - /// /// Plugin-scoped version of a Condition service. /// @@ -159,7 +142,7 @@ internal sealed partial class Condition : IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition +internal class ConditionPluginScoped : IInternalDisposableService, ICondition { [ServiceManager.ServiceDependency] private readonly Condition conditionService = Service.Get(); @@ -185,7 +168,7 @@ internal class ConditionPluginScoped : IDisposable, IServiceType, ICondition public bool this[int flag] => this.conditionService[flag]; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.conditionService.ConditionChange -= this.ConditionChangedForward; diff --git a/Dalamud/Game/ClientState/GamePad/GamepadState.cs b/Dalamud/Game/ClientState/GamePad/GamepadState.cs index 40e632113..a0e16f0e2 100644 --- a/Dalamud/Game/ClientState/GamePad/GamepadState.cs +++ b/Dalamud/Game/ClientState/GamePad/GamepadState.cs @@ -21,7 +21,7 @@ namespace Dalamud.Game.ClientState.GamePad; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState +internal unsafe class GamepadState : IInternalDisposableService, IGamepadState { private readonly Hook? gamepadPoll; @@ -109,7 +109,7 @@ internal unsafe class GamepadState : IDisposable, IServiceType, IGamepadState /// /// Disposes this instance, alongside its hooks. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.Dispose(true); GC.SuppressFinalize(this); diff --git a/Dalamud/Game/Command/CommandManager.cs b/Dalamud/Game/Command/CommandManager.cs index 6b67f1892..7dcca763b 100644 --- a/Dalamud/Game/Command/CommandManager.cs +++ b/Dalamud/Game/Command/CommandManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Command; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class CommandManager : IServiceType, IDisposable, ICommandManager +internal sealed class CommandManager : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -130,7 +130,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.chatGui.CheckMessageHandled -= this.OnCheckMessageHandled; } @@ -170,7 +170,7 @@ internal sealed class CommandManager : IServiceType, IDisposable, ICommandManage #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandManager +internal class CommandManagerPluginScoped : IInternalDisposableService, ICommandManager { private static readonly ModuleLog Log = new("Command"); @@ -193,7 +193,7 @@ internal class CommandManagerPluginScoped : IDisposable, IServiceType, ICommandM public ReadOnlyDictionary Commands => this.commandManagerService.Commands; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var command in this.pluginRegisteredCommands) { diff --git a/Dalamud/Game/Config/GameConfig.cs b/Dalamud/Game/Config/GameConfig.cs index 162df9417..a021025b1 100644 --- a/Dalamud/Game/Config/GameConfig.cs +++ b/Dalamud/Game/Config/GameConfig.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Config; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable +internal sealed class GameConfig : IInternalDisposableService, IGameConfig { private readonly TaskCompletionSource tcsInitialization = new(); private readonly TaskCompletionSource tcsSystem = new(); @@ -195,7 +195,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable public void Set(UiControlOption option, string value) => this.UiControl.Set(option.GetName(), value); /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { var ode = new ObjectDisposedException(nameof(GameConfig)); this.tcsInitialization.SetExceptionIfIncomplete(ode); @@ -248,7 +248,7 @@ internal sealed class GameConfig : IServiceType, IGameConfig, IDisposable #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig +internal class GameConfigPluginScoped : IInternalDisposableService, IGameConfig { [ServiceManager.ServiceDependency] private readonly GameConfig gameConfigService = Service.Get(); @@ -295,7 +295,7 @@ internal class GameConfigPluginScoped : IDisposable, IServiceType, IGameConfig public GameConfigSection UiControl => this.gameConfigService.UiControl; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameConfigService.Changed -= this.ConfigChangedForward; this.initializationTask.ContinueWith( diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index c4bda0d19..e2e4aef15 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -13,7 +13,7 @@ namespace Dalamud.Game.DutyState; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class DutyState : IDisposable, IServiceType, IDutyState +internal unsafe class DutyState : IInternalDisposableService, IDutyState { private readonly DutyStateAddressResolver address; private readonly Hook contentDirectorNetworkMessageHook; @@ -62,7 +62,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState private bool CompletedThisTerritory { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.contentDirectorNetworkMessageHook.Dispose(); this.framework.Update -= this.FrameworkOnUpdateEvent; @@ -168,7 +168,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState +internal class DutyStatePluginScoped : IInternalDisposableService, IDutyState { [ServiceManager.ServiceDependency] private readonly DutyState dutyStateService = Service.Get(); @@ -200,7 +200,7 @@ internal class DutyStatePluginScoped : IDisposable, IServiceType, IDutyState public bool IsDutyStarted => this.dutyStateService.IsDutyStarted; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.dutyStateService.DutyStarted -= this.DutyStartedForward; this.dutyStateService.DutyWiped -= this.DutyWipedForward; diff --git a/Dalamud/Game/Framework.cs b/Dalamud/Game/Framework.cs index 606bf03da..91b38348a 100644 --- a/Dalamud/Game/Framework.cs +++ b/Dalamud/Game/Framework.cs @@ -23,7 +23,7 @@ namespace Dalamud.Game; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class Framework : IDisposable, IServiceType, IFramework +internal sealed class Framework : IInternalDisposableService, IFramework { private static readonly ModuleLog Log = new("Framework"); @@ -279,7 +279,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.RunOnFrameworkThread(() => { @@ -476,7 +476,7 @@ internal sealed class Framework : IDisposable, IServiceType, IFramework #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework +internal class FrameworkPluginScoped : IInternalDisposableService, IFramework { [ServiceManager.ServiceDependency] private readonly Framework frameworkService = Service.Get(); @@ -511,7 +511,7 @@ internal class FrameworkPluginScoped : IDisposable, IServiceType, IFramework public bool IsFrameworkUnloading => this.frameworkService.IsFrameworkUnloading; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.frameworkService.Update -= this.OnUpdateForward; diff --git a/Dalamud/Game/Gui/ChatGui.cs b/Dalamud/Game/Gui/ChatGui.cs index 02b52ee56..e0b90b382 100644 --- a/Dalamud/Game/Gui/ChatGui.cs +++ b/Dalamud/Game/Gui/ChatGui.cs @@ -29,7 +29,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui +internal sealed unsafe class ChatGui : IInternalDisposableService, IChatGui { private static readonly ModuleLog Log = new("ChatGui"); @@ -109,7 +109,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.printMessageHook.Dispose(); this.populateItemLinkHook.Dispose(); @@ -409,7 +409,7 @@ internal sealed unsafe class ChatGui : IDisposable, IServiceType, IChatGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui +internal class ChatGuiPluginScoped : IInternalDisposableService, IChatGui { [ServiceManager.ServiceDependency] private readonly ChatGui chatGuiService = Service.Get(); @@ -447,7 +447,7 @@ internal class ChatGuiPluginScoped : IDisposable, IServiceType, IChatGui public IReadOnlyDictionary<(string PluginName, uint CommandId), Action> RegisteredLinkHandlers => this.chatGuiService.RegisteredLinkHandlers; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.chatGuiService.ChatMessage -= this.OnMessageForward; this.chatGuiService.CheckMessageHandled -= this.OnCheckMessageForward; diff --git a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs index 65c9b2760..f136d017a 100644 --- a/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs +++ b/Dalamud/Game/Gui/ContextMenu/ContextMenu.cs @@ -28,7 +28,7 @@ namespace Dalamud.Game.Gui.ContextMenu; /// [InterfaceVersion("1.0")] [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMenu +internal sealed unsafe class ContextMenu : IInternalDisposableService, IContextMenu { private static readonly ModuleLog Log = new("ContextMenu"); @@ -77,7 +77,7 @@ internal sealed unsafe class ContextMenu : IDisposable, IServiceType, IContextMe private IReadOnlyList? SubmenuItems { get; set; } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var manager = RaptureAtkUnitManager.Instance(); var menu = manager->GetAddonByName("ContextMenu"); @@ -496,7 +496,7 @@ original: #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu +internal class ContextMenuPluginScoped : IInternalDisposableService, IContextMenu { [ServiceManager.ServiceDependency] private readonly ContextMenu parentService = Service.Get(); @@ -514,7 +514,7 @@ internal class ContextMenuPluginScoped : IDisposable, IServiceType, IContextMenu private object MenuItemsLock { get; } = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.parentService.OnMenuOpened -= this.OnMenuOpenedForward; diff --git a/Dalamud/Game/Gui/Dtr/DtrBar.cs b/Dalamud/Game/Gui/Dtr/DtrBar.cs index 993bb951f..dbf6fba3c 100644 --- a/Dalamud/Game/Gui/Dtr/DtrBar.cs +++ b/Dalamud/Game/Gui/Dtr/DtrBar.cs @@ -22,7 +22,7 @@ namespace Dalamud.Game.Gui.Dtr; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar +internal sealed unsafe class DtrBar : IInternalDisposableService, IDtrBar { private const uint BaseNodeId = 1000; @@ -101,7 +101,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.addonLifecycle.UnregisterListener(this.dtrPostDrawListener); this.addonLifecycle.UnregisterListener(this.dtrPostRequestedUpdateListener); @@ -493,7 +493,7 @@ internal sealed unsafe class DtrBar : IDisposable, IServiceType, IDtrBar #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar +internal class DtrBarPluginScoped : IInternalDisposableService, IDtrBar { [ServiceManager.ServiceDependency] private readonly DtrBar dtrBarService = Service.Get(); @@ -501,7 +501,7 @@ internal class DtrBarPluginScoped : IDisposable, IServiceType, IDtrBar private readonly Dictionary pluginEntries = new(); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs index 2383b4e53..9310529e4 100644 --- a/Dalamud/Game/Gui/FlyText/FlyTextGui.cs +++ b/Dalamud/Game/Gui/FlyText/FlyTextGui.cs @@ -16,7 +16,7 @@ namespace Dalamud.Game.Gui.FlyText; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui +internal sealed class FlyTextGui : IInternalDisposableService, IFlyTextGui { /// /// The native function responsible for adding fly text to the UI. See . @@ -78,7 +78,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.createFlyTextHook.Dispose(); } @@ -277,7 +277,7 @@ internal sealed class FlyTextGui : IDisposable, IServiceType, IFlyTextGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui +internal class FlyTextGuiPluginScoped : IInternalDisposableService, IFlyTextGui { [ServiceManager.ServiceDependency] private readonly FlyTextGui flyTextGuiService = Service.Get(); @@ -294,7 +294,7 @@ internal class FlyTextGuiPluginScoped : IDisposable, IServiceType, IFlyTextGui public event IFlyTextGui.OnFlyTextCreatedDelegate? FlyTextCreated; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.flyTextGuiService.FlyTextCreated -= this.FlyTextCreatedForward; diff --git a/Dalamud/Game/Gui/GameGui.cs b/Dalamud/Game/Gui/GameGui.cs index a97e19a0a..9272aa824 100644 --- a/Dalamud/Game/Gui/GameGui.cs +++ b/Dalamud/Game/Gui/GameGui.cs @@ -27,7 +27,7 @@ namespace Dalamud.Game.Gui; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui +internal sealed unsafe class GameGui : IInternalDisposableService, IGameGui { private static readonly ModuleLog Log = new("GameGui"); @@ -344,7 +344,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui /// /// Disables the hooks and submodules of this module. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.setGlobalBgmHook.Dispose(); this.handleItemHoverHook.Dispose(); @@ -520,7 +520,7 @@ internal sealed unsafe class GameGui : IDisposable, IServiceType, IGameGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui +internal class GameGuiPluginScoped : IInternalDisposableService, IGameGui { [ServiceManager.ServiceDependency] private readonly GameGui gameGuiService = Service.Get(); @@ -558,7 +558,7 @@ internal class GameGuiPluginScoped : IDisposable, IServiceType, IGameGui public HoveredAction HoveredAction => this.gameGuiService.HoveredAction; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameGuiService.UiHideToggled -= this.UiHideToggledForward; this.gameGuiService.HoveredItemChanged -= this.HoveredItemForward; diff --git a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs index 4a8332d24..f19fe3b0a 100644 --- a/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs +++ b/Dalamud/Game/Gui/PartyFinder/PartyFinderGui.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Gui.PartyFinder; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGui +internal sealed class PartyFinderGui : IInternalDisposableService, IPartyFinderGui { private readonly PartyFinderAddressResolver address; private readonly IntPtr memory; @@ -47,7 +47,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu /// /// Dispose of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.receiveListingHook.Dispose(); @@ -131,7 +131,7 @@ internal sealed class PartyFinderGui : IDisposable, IServiceType, IPartyFinderGu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFinderGui +internal class PartyFinderGuiPluginScoped : IInternalDisposableService, IPartyFinderGui { [ServiceManager.ServiceDependency] private readonly PartyFinderGui partyFinderGuiService = Service.Get(); @@ -148,7 +148,7 @@ internal class PartyFinderGuiPluginScoped : IDisposable, IServiceType, IPartyFin public event IPartyFinderGui.PartyFinderListingEventDelegate? ReceiveListing; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.partyFinderGuiService.ReceiveListing -= this.ReceiveListingForward; diff --git a/Dalamud/Game/Gui/Toast/ToastGui.cs b/Dalamud/Game/Gui/Toast/ToastGui.cs index 7491b7f13..2cf327007 100644 --- a/Dalamud/Game/Gui/Toast/ToastGui.cs +++ b/Dalamud/Game/Gui/Toast/ToastGui.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.Gui.Toast; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui +internal sealed partial class ToastGui : IInternalDisposableService, IToastGui { private const uint QuestToastCheckmarkMagic = 60081; @@ -73,7 +73,7 @@ internal sealed partial class ToastGui : IDisposable, IServiceType, IToastGui /// /// Disposes of managed and unmanaged resources. /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.showNormalToastHook.Dispose(); this.showQuestToastHook.Dispose(); @@ -383,7 +383,7 @@ internal sealed partial class ToastGui #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui +internal class ToastGuiPluginScoped : IInternalDisposableService, IToastGui { [ServiceManager.ServiceDependency] private readonly ToastGui toastGuiService = Service.Get(); @@ -408,7 +408,7 @@ internal class ToastGuiPluginScoped : IDisposable, IServiceType, IToastGui public event IToastGui.OnErrorToastDelegate? ErrorToast; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.toastGuiService.Toast -= this.ToastForward; this.toastGuiService.QuestToast -= this.QuestToastForward; diff --git a/Dalamud/Game/Internal/AntiDebug.cs b/Dalamud/Game/Internal/AntiDebug.cs index 2f4ec28c0..5ab024012 100644 --- a/Dalamud/Game/Internal/AntiDebug.cs +++ b/Dalamud/Game/Internal/AntiDebug.cs @@ -12,7 +12,7 @@ namespace Dalamud.Game.Internal; /// This class disables anti-debug functionality in the game client. /// [ServiceManager.EarlyLoadedService] -internal sealed partial class AntiDebug : IServiceType +internal sealed class AntiDebug : IInternalDisposableService { private readonly byte[] nop = new byte[] { 0x31, 0xC0, 0x90, 0x90, 0x90, 0x90 }; private byte[] original; @@ -43,16 +43,25 @@ internal sealed partial class AntiDebug : IServiceType } } + /// Finalizes an instance of the class. + ~AntiDebug() => this.Disable(); + /// /// Gets a value indicating whether the anti-debugging is enabled. /// public bool IsEnabled { get; private set; } = false; + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// /// Enables the anti-debugging by overwriting code in memory. /// public void Enable() { + if (this.IsEnabled) + return; + this.original = new byte[this.nop.Length]; if (this.debugCheckAddress != IntPtr.Zero && !this.IsEnabled) { @@ -73,6 +82,9 @@ internal sealed partial class AntiDebug : IServiceType /// public void Disable() { + if (!this.IsEnabled) + return; + if (this.debugCheckAddress != IntPtr.Zero && this.original != null) { Log.Information($"Reverting debug check at 0x{this.debugCheckAddress.ToInt64():X}"); @@ -86,45 +98,3 @@ internal sealed partial class AntiDebug : IServiceType this.IsEnabled = false; } } - -/// -/// Implementing IDisposable. -/// -internal sealed partial class AntiDebug : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~AntiDebug() => this.Dispose(false); - - /// - /// Disposes of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes of managed and unmanaged resources. - /// - /// If this was disposed through calling Dispose() or from being finalized. - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - // If anti-debug is enabled and is being disposed, odds are either the game is exiting, or Dalamud is being reloaded. - // If it is the latter, there's half a chance a debugger is currently attached. There's no real need to disable the - // check in either situation anyways. However if Dalamud is being reloaded, the sig may fail so may as well undo it. - this.Disable(); - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Internal/DalamudAtkTweaks.cs b/Dalamud/Game/Internal/DalamudAtkTweaks.cs index 30fab6b1b..9f9328de1 100644 --- a/Dalamud/Game/Internal/DalamudAtkTweaks.cs +++ b/Dalamud/Game/Internal/DalamudAtkTweaks.cs @@ -20,7 +20,7 @@ namespace Dalamud.Game.Internal; /// This class implements in-game Dalamud options in the in-game System menu. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe partial class DalamudAtkTweaks : IServiceType +internal sealed unsafe class DalamudAtkTweaks : IInternalDisposableService { private readonly AtkValueChangeType atkValueChangeType; private readonly AtkValueSetString atkValueSetString; @@ -40,6 +40,8 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private readonly string locDalamudPlugins; private readonly string locDalamudSettings; + private bool disposed = false; + [ServiceManager.ServiceConstructor] private DalamudAtkTweaks(TargetSigScanner sigScanner) { @@ -69,6 +71,9 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType this.hookAtkUnitBaseReceiveGlobalEvent.Enable(); } + /// Finalizes an instance of the class. + ~DalamudAtkTweaks() => this.Dispose(false); + private delegate void AgentHudOpenSystemMenuPrototype(void* thisPtr, AtkValue* atkValueArgs, uint menuSize); private delegate void AtkValueChangeType(AtkValue* thisPtr, ValueType type); @@ -79,6 +84,26 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType private delegate IntPtr AtkUnitBaseReceiveGlobalEvent(AtkUnitBase* thisPtr, ushort cmd, uint a3, IntPtr a4, uint* a5); + /// + void IInternalDisposableService.DisposeService() => this.Dispose(true); + + private void Dispose(bool disposing) + { + if (this.disposed) + return; + + if (disposing) + { + this.hookAgentHudOpenSystemMenu.Dispose(); + this.hookUiModuleRequestMainCommand.Dispose(); + this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); + + // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; + } + + this.disposed = true; + } + /* private void ContextMenuOnContextMenuOpened(ContextMenuOpenedArgs args) { @@ -229,45 +254,3 @@ internal sealed unsafe partial class DalamudAtkTweaks : IServiceType } } } - -/// -/// Implements IDisposable. -/// -internal sealed partial class DalamudAtkTweaks : IDisposable -{ - private bool disposed = false; - - /// - /// Finalizes an instance of the class. - /// - ~DalamudAtkTweaks() => this.Dispose(false); - - /// - /// Dispose of managed and unmanaged resources. - /// - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Dispose of managed and unmanaged resources. - /// - private void Dispose(bool disposing) - { - if (this.disposed) - return; - - if (disposing) - { - this.hookAgentHudOpenSystemMenu.Dispose(); - this.hookUiModuleRequestMainCommand.Dispose(); - this.hookAtkUnitBaseReceiveGlobalEvent.Dispose(); - - // this.contextMenu.ContextMenuOpened -= this.ContextMenuOnContextMenuOpened; - } - - this.disposed = true; - } -} diff --git a/Dalamud/Game/Inventory/GameInventory.cs b/Dalamud/Game/Inventory/GameInventory.cs index 1c7f3e3bf..3e3dbc685 100644 --- a/Dalamud/Game/Inventory/GameInventory.cs +++ b/Dalamud/Game/Inventory/GameInventory.cs @@ -19,7 +19,7 @@ namespace Dalamud.Game.Inventory; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal class GameInventory : IDisposable, IServiceType +internal class GameInventory : IInternalDisposableService { private readonly List subscribersPendingChange = new(); private readonly List subscribers = new(); @@ -61,7 +61,7 @@ internal class GameInventory : IDisposable, IServiceType private unsafe delegate void RaptureAtkModuleUpdateDelegate(RaptureAtkModule* ram, float f1); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.subscribersPendingChange) { @@ -351,7 +351,7 @@ internal class GameInventory : IDisposable, IServiceType #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInventory +internal class GameInventoryPluginScoped : IInternalDisposableService, IGameInventory { private static readonly ModuleLog Log = new(nameof(GameInventoryPluginScoped)); @@ -406,7 +406,7 @@ internal class GameInventoryPluginScoped : IDisposable, IServiceType, IGameInven public event IGameInventory.InventoryChangedDelegate? ItemMergedExplicit; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameInventoryService.Unsubscribe(this); diff --git a/Dalamud/Game/Network/GameNetwork.cs b/Dalamud/Game/Network/GameNetwork.cs index 4099f228e..954612af7 100644 --- a/Dalamud/Game/Network/GameNetwork.cs +++ b/Dalamud/Game/Network/GameNetwork.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game.Network; /// [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork +internal sealed class GameNetwork : IInternalDisposableService, IGameNetwork { private readonly GameNetworkAddressResolver address; private readonly Hook processZonePacketDownHook; @@ -59,7 +59,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.processZonePacketDownHook.Dispose(); this.processZonePacketUpHook.Dispose(); @@ -145,7 +145,7 @@ internal sealed class GameNetwork : IDisposable, IServiceType, IGameNetwork #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork +internal class GameNetworkPluginScoped : IInternalDisposableService, IGameNetwork { [ServiceManager.ServiceDependency] private readonly GameNetwork gameNetworkService = Service.Get(); @@ -162,7 +162,7 @@ internal class GameNetworkPluginScoped : IDisposable, IServiceType, IGameNetwork public event IGameNetwork.OnNetworkMessageDelegate? NetworkMessage; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.gameNetworkService.NetworkMessage -= this.NetworkMessageForward; diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 8d5ec1344..2a46af3d3 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -26,7 +26,7 @@ namespace Dalamud.Game.Network.Internal; /// This class handles network notifications and uploading market board data. /// [ServiceManager.BlockingEarlyLoadedService] -internal unsafe class NetworkHandlers : IDisposable, IServiceType +internal unsafe class NetworkHandlers : IInternalDisposableService { private readonly IMarketBoardUploader uploader; @@ -213,7 +213,7 @@ internal unsafe class NetworkHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.disposing = true; this.Dispose(this.disposing); diff --git a/Dalamud/Game/Network/Internal/WinSockHandlers.cs b/Dalamud/Game/Network/Internal/WinSockHandlers.cs index 8439389ff..619c458c4 100644 --- a/Dalamud/Game/Network/Internal/WinSockHandlers.cs +++ b/Dalamud/Game/Network/Internal/WinSockHandlers.cs @@ -10,7 +10,7 @@ namespace Dalamud.Game.Network.Internal; /// This class enables TCP optimizations in the game socket for better performance. /// [ServiceManager.EarlyLoadedService] -internal sealed class WinSockHandlers : IDisposable, IServiceType +internal sealed class WinSockHandlers : IInternalDisposableService { private Hook ws2SocketHook; @@ -27,7 +27,7 @@ internal sealed class WinSockHandlers : IDisposable, IServiceType /// /// Disposes of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.ws2SocketHook?.Dispose(); } diff --git a/Dalamud/Game/SigScanner.cs b/Dalamud/Game/SigScanner.cs index fe2d9083e..5e49052ae 100644 --- a/Dalamud/Game/SigScanner.cs +++ b/Dalamud/Game/SigScanner.cs @@ -104,6 +104,10 @@ public class SigScanner : IDisposable, ISigScanner /// public ProcessModule Module { get; } + /// Gets or sets a value indicating whether this instance of is meant to be a + /// Dalamud service. + private protected bool IsService { get; set; } + private IntPtr TextSectionTop => this.TextSectionBase + this.TextSectionSize; /// @@ -309,13 +313,11 @@ public class SigScanner : IDisposable, ISigScanner } } - /// - /// Free the memory of the copied module search area on object disposal, if applicable. - /// + /// public void Dispose() { - this.Save(); - Marshal.FreeHGlobal(this.moduleCopyPtr); + if (!this.IsService) + this.DisposeCore(); } /// @@ -337,6 +339,15 @@ public class SigScanner : IDisposable, ISigScanner } } + /// + /// Free the memory of the copied module search area on object disposal, if applicable. + /// + private protected void DisposeCore() + { + this.Save(); + Marshal.FreeHGlobal(this.moduleCopyPtr); + } + /// /// Helper for ScanText to get the correct address for IDA sigs that mark the first JMP or CALL location. /// diff --git a/Dalamud/Game/TargetSigScanner.cs b/Dalamud/Game/TargetSigScanner.cs index 35c82562e..e169ea904 100644 --- a/Dalamud/Game/TargetSigScanner.cs +++ b/Dalamud/Game/TargetSigScanner.cs @@ -15,7 +15,7 @@ namespace Dalamud.Game; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TargetSigScanner : SigScanner, IServiceType +internal class TargetSigScanner : SigScanner, IPublicDisposableService { /// /// Initializes a new instance of the class. @@ -26,4 +26,14 @@ internal class TargetSigScanner : SigScanner, IServiceType : base(Process.GetCurrentProcess().MainModule!, doCopy, cacheFile) { } + + /// + void IInternalDisposableService.DisposeService() + { + if (this.IsService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.IsService = true; } diff --git a/Dalamud/Game/Text/SeStringHandling/SeString.cs b/Dalamud/Game/Text/SeStringHandling/SeString.cs index 47c38b227..91dceb5d1 100644 --- a/Dalamud/Game/Text/SeStringHandling/SeString.cs +++ b/Dalamud/Game/Text/SeStringHandling/SeString.cs @@ -381,7 +381,7 @@ public class SeString { new PartyFinderPayload(listingId, isCrossWorld ? PartyFinderPayload.PartyFinderLinkType.NotSpecified : PartyFinderPayload.PartyFinderLinkType.LimitedToHomeWorld), // -> - new TextPayload($"Looking for Party ({recruiterName})"), + new TextPayload($"Looking for Party ({recruiterName})" + (isCrossWorld ? " " : string.Empty)), }; payloads.InsertRange(1, TextArrowPayloads); diff --git a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs index 9958385b9..1138d4e07 100644 --- a/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs +++ b/Dalamud/Hooking/Internal/GameInteropProviderPluginScoped.cs @@ -21,7 +21,7 @@ namespace Dalamud.Hooking.Internal; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceType, IDisposable +internal class GameInteropProviderPluginScoped : IGameInteropProvider, IInternalDisposableService { private readonly LocalPlugin plugin; private readonly SigScanner scanner; @@ -83,7 +83,7 @@ internal class GameInteropProviderPluginScoped : IGameInteropProvider, IServiceT => this.HookFromAddress(this.scanner.ScanText(signature), detour, backend); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var notDisposed = this.trackedHooks.Where(x => !x.IsDisposed).ToArray(); if (notDisposed.Length != 0) diff --git a/Dalamud/Hooking/Internal/HookManager.cs b/Dalamud/Hooking/Internal/HookManager.cs index 9c288a276..c8cdf3a46 100644 --- a/Dalamud/Hooking/Internal/HookManager.cs +++ b/Dalamud/Hooking/Internal/HookManager.cs @@ -14,7 +14,7 @@ namespace Dalamud.Hooking.Internal; /// This class manages the final disposition of hooks, cleaning up any that have not reverted their changes. /// [ServiceManager.EarlyLoadedService] -internal class HookManager : IDisposable, IServiceType +internal class HookManager : IInternalDisposableService { /// /// Logger shared with . @@ -74,7 +74,7 @@ internal class HookManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { RevertHooks(); TrackedHooks.Clear(); diff --git a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs index 91020f898..a2253eb23 100644 --- a/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs +++ b/Dalamud/Hooking/WndProcHook/WndProcHookManager.cs @@ -15,7 +15,7 @@ namespace Dalamud.Hooking.WndProcHook; /// Manages WndProc hooks for game main window and extra ImGui viewport windows. /// [ServiceManager.BlockingEarlyLoadedService] -internal sealed class WndProcHookManager : IServiceType, IDisposable +internal sealed class WndProcHookManager : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(WndProcHookManager)); @@ -56,7 +56,7 @@ internal sealed class WndProcHookManager : IServiceType, IDisposable public event WndProcEventDelegate? PostWndProc; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (this.dispatchMessageWHook.IsDisposed) return; diff --git a/Dalamud/IServiceType.cs b/Dalamud/IServiceType.cs index 973795faf..3a5dde880 100644 --- a/Dalamud/IServiceType.cs +++ b/Dalamud/IServiceType.cs @@ -6,3 +6,20 @@ public interface IServiceType { } + +/// , but for . +/// Use this to prevent services from accidentally being disposed by plugins or using clauses. +internal interface IInternalDisposableService : IServiceType +{ + /// Disposes the service. + void DisposeService(); +} + +/// An which happens to be public and needs to expose +/// . +internal interface IPublicDisposableService : IInternalDisposableService, IDisposable +{ + /// Marks that only should respond, + /// while suppressing . + void MarkDisposeOnlyFromService(); +} diff --git a/Dalamud/Interface/DragDrop/DragDropManager.cs b/Dalamud/Interface/DragDrop/DragDropManager.cs index 151ef28a0..adc0ebff7 100644 --- a/Dalamud/Interface/DragDrop/DragDropManager.cs +++ b/Dalamud/Interface/DragDrop/DragDropManager.cs @@ -19,7 +19,7 @@ namespace Dalamud.Interface.DragDrop; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal partial class DragDropManager : IDisposable, IDragDropManager, IServiceType +internal partial class DragDropManager : IInternalDisposableService, IDragDropManager { private nint windowHandlePtr = nint.Zero; @@ -56,6 +56,9 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService /// Gets the list of directory paths currently being dragged from an external application over any FFXIV-related viewport or stored from the last drop. public IReadOnlyList Directories { get; private set; } = Array.Empty(); + /// + void IInternalDisposableService.DisposeService() => this.Disable(); + /// Enable external drag and drop. public void Enable() { @@ -99,10 +102,6 @@ internal partial class DragDropManager : IDisposable, IDragDropManager, IService this.ServiceAvailable = false; } - /// - public void Dispose() - => this.Disable(); - /// public void CreateImGuiSource(string label, Func validityCheck, Func tooltipBuilder) { diff --git a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs index 9420fe42c..7636f22b6 100644 --- a/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs +++ b/Dalamud/Interface/ImGuiFontChooserDialog/SingleFontChooserDialog.cs @@ -93,7 +93,7 @@ public sealed class SingleFontChooserDialog : IDisposable /// Initializes a new instance of the class. /// A new instance of created using /// as its auto-rebuild mode. - /// The passed instance of will be disposed after use. If you pass an atlas + /// The passed instance of will be disposed after use. If you pass an atlas /// that is already being used, then all the font handles under the passed atlas will be invalidated upon disposing /// this font chooser. Consider using for automatic /// handling of font atlas derived from a , or even for automatic diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs new file mode 100644 index 000000000..b85a96004 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationClickArgs.cs @@ -0,0 +1,9 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationClickArgs +{ + /// Gets the notification being clicked. + IActiveNotification Notification { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs new file mode 100644 index 000000000..7f664efa1 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDismissArgs.cs @@ -0,0 +1,12 @@ +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDismissArgs +{ + /// Gets the notification being dismissed. + IActiveNotification Notification { get; } + + /// Gets the dismiss reason. + NotificationDismissReason Reason { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs new file mode 100644 index 000000000..221f769e0 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/EventArgs/INotificationDrawArgs.cs @@ -0,0 +1,19 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.EventArgs; + +/// Arguments for use with . +/// Not to be implemented by plugins. +public interface INotificationDrawArgs +{ + /// Gets the notification being drawn. + IActiveNotification Notification { get; } + + /// Gets the top left coordinates of the area being drawn. + Vector2 MinCoord { get; } + + /// Gets the bottom right coordinates of the area being drawn. + /// Note that can be , in which case there is no + /// vertical limits to the drawing region. + Vector2 MaxCoord { get; } +} diff --git a/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs new file mode 100644 index 000000000..e677471b4 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/IActiveNotification.cs @@ -0,0 +1,83 @@ +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.ImGuiNotification.EventArgs; +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Represents an active notification. +/// Not to be implemented by plugins. +public interface IActiveNotification : INotification +{ + /// The counter for field. + private static long idCounter; + + /// Invoked upon dismissing the notification. + /// The event callback will not be called, if it gets dismissed after plugin unload. + event Action Dismiss; + + /// Invoked upon clicking on the notification. + /// Note that this function may be called even after has been invoked. + event Action Click; + + /// Invoked upon drawing the action bar of the notification. + /// Note that this function may be called even after has been invoked. + event Action DrawActions; + + /// Gets the ID of this notification. + /// This value does not change. + long Id { get; } + + /// Gets the time of creating this notification. + /// This value does not change. + DateTime CreatedAt { get; } + + /// Gets the effective expiry time. + /// Contains if the notification does not expire. + /// This value will change depending on property changes and user interactions. + DateTime EffectiveExpiry { get; } + + /// Gets the reason how this notification got dismissed. null if not dismissed. + /// This includes when the hide animation is being played. + NotificationDismissReason? DismissReason { get; } + + /// Dismisses this notification. + /// If the notification has already been dismissed, this function does nothing. + void DismissNow(); + + /// Extends this notifiation. + /// The extension time. + /// This does not override . + void ExtendBy(TimeSpan extension); + + /// Sets the icon from , overriding the icon. + /// The new texture wrap to use, or null to clear and revert back to the icon specified + /// from . + /// + /// The texture passed will be disposed when the notification is dismissed or a new different texture is set + /// via another call to this function. You do not have to dispose it yourself. + /// If is not null, then calling this function will simply dispose the + /// passed without actually updating the icon. + /// + void SetIconTexture(IDalamudTextureWrap? textureWrap); + + /// Sets the icon from , overriding the icon, once the given task + /// completes. + /// The task that will result in a new texture wrap to use, or null to clear and + /// revert back to the icon specified from . + /// + /// The texture resulted from the passed will be disposed when the notification + /// is dismissed or a new different texture is set via another call to this function. You do not have to dispose the + /// resulted instance of yourself. + /// If the task fails for any reason, the exception will be silently ignored and the icon specified from + /// will be used instead. + /// If is not null, then calling this function will simply dispose the + /// result of the passed without actually updating the icon. + /// + void SetIconTexture(Task? textureWrapTask); + + /// Generates a new value to use for . + /// The new value. + internal static long CreateNewId() => Interlocked.Increment(ref idCounter); +} diff --git a/Dalamud/Interface/ImGuiNotification/INotification.cs b/Dalamud/Interface/ImGuiNotification/INotification.cs new file mode 100644 index 000000000..f9a043c0b --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotification.cs @@ -0,0 +1,76 @@ +using System.Threading.Tasks; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Represents a notification. +/// Not to be implemented by plugins. +public interface INotification +{ + /// Gets or sets the content body of the notification. + string Content { get; set; } + + /// Gets or sets the title of the notification. + string? Title { get; set; } + + /// Gets or sets the text to display when the notification is minimized. + string? MinimizedText { get; set; } + + /// Gets or sets the type of the notification. + NotificationType Type { get; set; } + + /// Gets or sets the icon source. + /// Use or + /// to use a texture, after calling + /// . Call either of those functions with null to revert + /// the effective icon back to this property. + INotificationIcon? Icon { get; set; } + + /// Gets or sets the hard expiry. + /// + /// Setting this value will override and , in that + /// the notification will be dismissed when this expiry expires.
+ /// Set to to make only take effect.
+ /// If neither nor is not MaxValue, then the notification + /// will not expire after a set time. It must be explicitly dismissed by the user of via calling + /// .
+ /// Updating this value will reset the dismiss timer. + ///
+ DateTime HardExpiry { get; set; } + + /// Gets or sets the initial duration. + /// Set to to make only take effect. + /// Updating this value will reset the dismiss timer, but the remaining duration will still be calculated + /// based on . + TimeSpan InitialDuration { get; set; } + + /// Gets or sets the new duration for this notification once the mouse cursor leaves the window and the + /// window is no longer focused. + /// + /// If set to or less, then this feature is turned off, and hovering the mouse on the + /// notification or focusing on it will not make the notification stay.
+ /// Updating this value will reset the dismiss timer. + ///
+ TimeSpan ExtensionDurationSinceLastInterest { get; set; } + + /// Gets or sets a value indicating whether to show an indeterminate expiration animation if + /// is set to . + bool ShowIndeterminateIfNoExpiry { get; set; } + + /// Gets or sets a value indicating whether to respect the current UI visibility state. + bool RespectUiHidden { get; set; } + + /// Gets or sets a value indicating whether the notification has been minimized. + bool Minimized { get; set; } + + /// Gets or sets a value indicating whether the user can dismiss the notification by themselves. + /// Consider adding a cancel button to . + bool UserDismissable { get; set; } + + /// Gets or sets the progress for the background progress bar of the notification. + /// The progress should be in the range between 0 and 1. + float Progress { get; set; } +} diff --git a/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs new file mode 100644 index 000000000..94c746b4f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/INotificationIcon.cs @@ -0,0 +1,54 @@ +using System.Numerics; +using System.Runtime.CompilerServices; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Icon source for . +/// Plugins implementing this interface are left to their own on managing the resources contained by the +/// instance of their implementation of . In other words, they should not expect to have +/// called if their implementation is an . Dalamud will not +/// call on any instance of . On plugin unloads, the +/// icon may be reverted back to the default, if the instance of is not provided by +/// Dalamud. +public interface INotificationIcon +{ + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(SeIconChar iconChar) => new SeIconCharNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from an + /// . + /// The icon character. + /// A new instance of that should be disposed after use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon From(FontAwesomeIcon iconChar) => new FontAwesomeIconNotificationIcon(iconChar); + + /// Gets a new instance of that will source the icon from a texture + /// file shipped as a part of the game resources. + /// The path to a texture file in the game virtual file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromGame(string gamePath) => new GamePathNotificationIcon(gamePath); + + /// Gets a new instance of that will source the icon from an image + /// file from the file system. + /// The path to an image file in the file system. + /// A new instance of that should be disposed after use. + /// If any errors are thrown, the default icon will be displayed instead. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon FromFile(string filePath) => new FilePathNotificationIcon(filePath); + + /// Draws the icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The foreground color. + /// true if anything has been drawn. + bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color); +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs new file mode 100644 index 000000000..428d9103f --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.EventArgs.cs @@ -0,0 +1,87 @@ +using System.Numerics; + +using Dalamud.Interface.ImGuiNotification.EventArgs; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDismissArgs +{ + /// + public event Action? Dismiss; + + /// + IActiveNotification INotificationDismissArgs.Notification => this; + + /// + NotificationDismissReason INotificationDismissArgs.Reason => + this.DismissReason + ?? throw new InvalidOperationException("DismissReason must be set before using INotificationDismissArgs"); + + private void InvokeDismiss() + { + try + { + this.Dismiss?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Dismiss)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationClickArgs +{ + /// + public event Action? Click; + + /// + IActiveNotification INotificationClickArgs.Notification => this; + + private void InvokeClick() + { + try + { + this.Click?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.Click)} error"); + } + } +} + +/// Represents an active notification. +internal sealed partial class ActiveNotification : INotificationDrawArgs +{ + private Vector2 drawActionArgMinCoord; + private Vector2 drawActionArgMaxCoord; + + /// + public event Action? DrawActions; + + /// + IActiveNotification INotificationDrawArgs.Notification => this; + + /// + Vector2 INotificationDrawArgs.MinCoord => this.drawActionArgMinCoord; + + /// + Vector2 INotificationDrawArgs.MaxCoord => this.drawActionArgMaxCoord; + + private void InvokeDrawActions(Vector2 minCoord, Vector2 maxCoord) + { + this.drawActionArgMinCoord = minCoord; + this.drawActionArgMaxCoord = maxCoord; + try + { + this.DrawActions?.Invoke(this); + } + catch (Exception e) + { + this.LogEventInvokeError(e, $"{nameof(this.DrawActions)} error; event registration cancelled"); + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs new file mode 100644 index 000000000..d4a08ff69 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.ImGui.cs @@ -0,0 +1,500 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Interface.Utility; +using Dalamud.Utility; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification +{ + /// Draws this notification. + /// The maximum width of the notification window. + /// The offset from the bottom. + /// The height of the notification. + public float Draw(float width, float offsetY) + { + var opacity = + Math.Clamp( + (float)(this.hideEasing.IsRunning + ? (this.hideEasing.IsDone ? 0 : 1f - this.hideEasing.Value) + : (this.showEasing.IsDone ? 1 : this.showEasing.Value)), + 0f, + 1f); + if (opacity <= 0) + return 0; + + var actionWindowHeight = + // Content + ImGui.GetTextLineHeight() + + // Top and bottom padding + (NotificationConstants.ScaledWindowPadding * 2); + + var viewport = ImGuiHelpers.MainViewport; + var viewportPos = viewport.WorkPos; + var viewportSize = viewport.WorkSize; + + ImGui.PushID(this.Id.GetHashCode()); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity); + ImGui.PushStyleVar(ImGuiStyleVar.WindowRounding, 0f); + ImGui.PushStyleVar(ImGuiStyleVar.WindowPadding, new Vector2(NotificationConstants.ScaledWindowPadding)); + unsafe + { + ImGui.PushStyleColor( + ImGuiCol.WindowBg, + *ImGui.GetStyleColorVec4(ImGuiCol.WindowBg) * new Vector4( + 1f, + 1f, + 1f, + NotificationConstants.BackgroundOpacity)); + } + + ImGuiHelpers.ForceNextWindowMainViewport(); + ImGui.SetNextWindowPos( + (viewportPos + viewportSize) - + new Vector2(NotificationConstants.ScaledViewportEdgeMargin) - + new Vector2(0, offsetY), + ImGuiCond.Always, + Vector2.One); + ImGui.SetNextWindowSizeConstraints( + new(width, actionWindowHeight), + new( + width, + !this.underlyingNotification.Minimized || this.expandoEasing.IsRunning + ? float.MaxValue + : actionWindowHeight)); + ImGui.Begin( + $"##NotifyMainWindow{this.Id}", + ImGuiWindowFlags.AlwaysAutoResize | + ImGuiWindowFlags.NoDecoration | + ImGuiWindowFlags.NoNav | + ImGuiWindowFlags.NoMove | + ImGuiWindowFlags.NoFocusOnAppearing | + ImGuiWindowFlags.NoDocking); + + var isFocused = ImGui.IsWindowFocused(); + var isHovered = ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem); + var isTakingKeyboardInput = isFocused && ImGui.GetIO().WantTextInput; + var warrantsExtension = + this.ExtensionDurationSinceLastInterest > TimeSpan.Zero + && (isHovered || isTakingKeyboardInput); + + this.EffectiveExpiry = this.CalculateEffectiveExpiry(ref warrantsExtension); + + if (!isTakingKeyboardInput && !isHovered && isFocused) + { + ImGui.SetWindowFocus(null); + isFocused = false; + } + + if (DateTime.Now > this.EffectiveExpiry) + this.DismissNow(NotificationDismissReason.Timeout); + + if (this.ExtensionDurationSinceLastInterest > TimeSpan.Zero && warrantsExtension) + this.lastInterestTime = DateTime.Now; + + this.DrawWindowBackgroundProgressBar(); + this.DrawTopBar(width, actionWindowHeight, isHovered); + if (!this.underlyingNotification.Minimized && !this.expandoEasing.IsRunning) + { + this.DrawContentAndActions(width, actionWindowHeight); + } + else if (this.expandoEasing.IsRunning) + { + if (this.underlyingNotification.Minimized) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (1f - (float)this.expandoEasing.Value)); + else + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, opacity * (float)this.expandoEasing.Value); + this.DrawContentAndActions(width, actionWindowHeight); + ImGui.PopStyleVar(); + } + + if (isFocused) + this.DrawFocusIndicator(); + this.DrawExpiryBar(this.EffectiveExpiry, warrantsExtension); + + if (ImGui.IsWindowHovered()) + { + if (this.Click is null) + { + if (this.UserDismissable && ImGui.IsMouseClicked(ImGuiMouseButton.Left)) + this.DismissNow(NotificationDismissReason.Manual); + } + else + { + if (ImGui.IsMouseClicked(ImGuiMouseButton.Left) + || ImGui.IsMouseClicked(ImGuiMouseButton.Right) + || ImGui.IsMouseClicked(ImGuiMouseButton.Middle)) + this.InvokeClick(); + } + } + + var windowSize = ImGui.GetWindowSize(); + ImGui.End(); + + ImGui.PopStyleColor(); + ImGui.PopStyleVar(3); + ImGui.PopID(); + + return windowSize.Y; + } + + /// Calculates the effective expiry, taking ImGui window state into account. + /// Notification will not dismiss while this paramter is true. + /// The calculated effective expiry. + /// Expected to be called BETWEEN and . + private DateTime CalculateEffectiveExpiry(ref bool warrantsExtension) + { + DateTime expiry; + var initialDuration = this.InitialDuration; + var expiryInitial = + initialDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.CreatedAt + initialDuration; + + var extendDuration = this.ExtensionDurationSinceLastInterest; + if (warrantsExtension) + { + expiry = DateTime.MaxValue; + } + else + { + var expiryExtend = + extendDuration == TimeSpan.MaxValue + ? DateTime.MaxValue + : this.lastInterestTime + extendDuration; + + expiry = expiryInitial > expiryExtend ? expiryInitial : expiryExtend; + if (expiry < this.extendedExpiry) + expiry = this.extendedExpiry; + } + + var he = this.HardExpiry; + if (he < expiry) + { + expiry = he; + warrantsExtension = false; + } + + return expiry; + } + + private void DrawWindowBackgroundProgressBar() + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.ProgressWaveLoopDuration) / + NotificationConstants.ProgressWaveLoopDuration); + elapsed /= NotificationConstants.ProgressWaveIdleTimeRatio; + + var colorElapsed = + elapsed < NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + ? elapsed / NotificationConstants.ProgressWaveLoopMaxColorTimeRatio + : ((NotificationConstants.ProgressWaveLoopMaxColorTimeRatio * 2) - elapsed) / + NotificationConstants.ProgressWaveLoopMaxColorTimeRatio; + + elapsed = Math.Clamp(elapsed, 0f, 1f); + colorElapsed = Math.Clamp(colorElapsed, 0f, 1f); + colorElapsed = MathF.Sin(colorElapsed * (MathF.PI / 2f)); + + var progress = Math.Clamp(this.ProgressEased, 0f, 1f); + if (progress >= 1f) + elapsed = colorElapsed = 0f; + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + var rb = windowPos + windowSize; + var midp = windowPos + windowSize with { X = windowSize.X * progress * elapsed }; + var rp = windowPos + windowSize with { X = windowSize.X * progress }; + + ImGui.PushClipRect(windowPos, rb, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos, + midp, + ImGui.GetColorU32( + Vector4.Lerp( + NotificationConstants.BackgroundProgressColorMin, + NotificationConstants.BackgroundProgressColorMax, + colorElapsed))); + ImGui.GetWindowDrawList().AddRectFilled( + midp with { Y = 0 }, + rp, + ImGui.GetColorU32(NotificationConstants.BackgroundProgressColorMin)); + ImGui.PopClipRect(); + } + + private void DrawFocusIndicator() + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRect( + windowPos, + windowPos + windowSize, + ImGui.GetColorU32(NotificationConstants.FocusBorderColor * new Vector4(1f, 1f, 1f, ImGui.GetStyle().Alpha)), + 0f, + ImDrawFlags.None, + NotificationConstants.FocusIndicatorThickness); + ImGui.PopClipRect(); + } + + private void DrawTopBar(float width, float height, bool drawActionButtons) + { + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + + var rtOffset = new Vector2(width, 0); + using (Service.Get().IconFontHandle?.Push()) + { + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + if (this.UserDismissable) + { + if (this.DrawIconButton(FontAwesomeIcon.Times, rtOffset, height, drawActionButtons)) + this.DismissNow(NotificationDismissReason.Manual); + rtOffset.X -= height; + } + + if (this.underlyingNotification.Minimized) + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronDown, rtOffset, height, drawActionButtons)) + this.Minimized = false; + } + else + { + if (this.DrawIconButton(FontAwesomeIcon.ChevronUp, rtOffset, height, drawActionButtons)) + this.Minimized = true; + } + + rtOffset.X -= height; + ImGui.PopClipRect(); + } + + float relativeOpacity; + if (this.expandoEasing.IsRunning) + { + relativeOpacity = + this.underlyingNotification.Minimized + ? 1f - (float)this.expandoEasing.Value + : (float)this.expandoEasing.Value; + } + else + { + relativeOpacity = this.underlyingNotification.Minimized ? 0f : 1f; + } + + if (drawActionButtons) + ImGui.PushClipRect(windowPos, windowPos + rtOffset with { Y = height }, false); + else + ImGui.PushClipRect(windowPos, windowPos + windowSize with { Y = height }, false); + + if (relativeOpacity > 0) + { + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * relativeOpacity); + ImGui.SetCursorPos(new(NotificationConstants.ScaledWindowPadding)); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted( + ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByActiveItem) + ? this.CreatedAt.LocAbsolute() + : this.CreatedAt.LocRelativePastLong()); + ImGui.PopStyleColor(); + ImGui.PopStyleVar(); + } + + if (relativeOpacity < 1) + { + rtOffset = new(width - NotificationConstants.ScaledWindowPadding, 0); + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, ImGui.GetStyle().Alpha * (1f - relativeOpacity)); + + var ltOffset = new Vector2(NotificationConstants.ScaledWindowPadding); + this.DrawIcon(ltOffset, new(height - (2 * NotificationConstants.ScaledWindowPadding))); + + ltOffset.X = height; + + var agoText = this.CreatedAt.LocRelativePastShort(); + var agoSize = ImGui.CalcTextSize(agoText); + rtOffset.X -= agoSize.X; + ImGui.SetCursorPos(rtOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.WhenTextColor); + ImGui.TextUnformatted(agoText); + ImGui.PopStyleColor(); + + rtOffset.X -= NotificationConstants.ScaledWindowPadding; + + ImGui.PushClipRect( + windowPos + ltOffset with { Y = 0 }, + windowPos + rtOffset with { Y = height }, + true); + ImGui.SetCursorPos(ltOffset with { Y = NotificationConstants.ScaledWindowPadding }); + ImGui.TextUnformatted(this.EffectiveMinimizedText); + ImGui.PopClipRect(); + + ImGui.PopStyleVar(); + } + + ImGui.PopClipRect(); + } + + private bool DrawIconButton(FontAwesomeIcon icon, Vector2 rt, float size, bool drawActionButtons) + { + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, Vector2.Zero); + if (!drawActionButtons) + ImGui.PushStyleVar(ImGuiStyleVar.Alpha, 0f); + ImGui.PushStyleColor(ImGuiCol.Button, 0); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.CloseTextColor); + + ImGui.SetCursorPos(rt - new Vector2(size, 0)); + var r = ImGui.Button(icon.ToIconString(), new(size)); + + ImGui.PopStyleColor(2); + if (!drawActionButtons) + ImGui.PopStyleVar(); + ImGui.PopStyleVar(); + return r; + } + + private void DrawContentAndActions(float width, float actionWindowHeight) + { + var textColumnX = (NotificationConstants.ScaledWindowPadding * 2) + NotificationConstants.ScaledIconSize; + var textColumnWidth = width - textColumnX - NotificationConstants.ScaledWindowPadding; + var textColumnOffset = new Vector2(textColumnX, actionWindowHeight); + + this.DrawIcon( + new(NotificationConstants.ScaledWindowPadding, actionWindowHeight), + new(NotificationConstants.ScaledIconSize)); + + textColumnOffset.Y += this.DrawTitle(textColumnOffset, textColumnWidth); + textColumnOffset.Y += NotificationConstants.ScaledComponentGap; + + this.DrawContentBody(textColumnOffset, textColumnWidth); + + if (this.DrawActions is null) + return; + + var userActionOffset = new Vector2( + NotificationConstants.ScaledWindowPadding, + ImGui.GetCursorPosY() + NotificationConstants.ScaledComponentGap); + ImGui.SetCursorPos(userActionOffset); + this.InvokeDrawActions( + userActionOffset, + new(width - NotificationConstants.ScaledWindowPadding, float.MaxValue)); + } + + private void DrawIcon(Vector2 minCoord, Vector2 size) + { + var maxCoord = minCoord + size; + var iconColor = this.Type.ToColor(); + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.iconTextureWrap)) + return; + + if (this.Icon?.DrawIcon(minCoord, maxCoord, iconColor) is true) + return; + + if (NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.Type.ToChar(), + Service.Get().IconFontAwesomeFontHandle, + iconColor)) + return; + + if (NotificationUtilities.DrawIconFrom(minCoord, maxCoord, this.initiatorPlugin)) + return; + + NotificationUtilities.DrawIconFromDalamudLogo(minCoord, maxCoord); + } + + private float DrawTitle(Vector2 minCoord, float width) + { + ImGui.PushTextWrapPos(minCoord.X + width); + + ImGui.SetCursorPos(minCoord); + if ((this.Title ?? this.Type.ToTitle()) is { } title) + { + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.TitleTextColor); + ImGui.TextUnformatted(title); + ImGui.PopStyleColor(); + } + + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BlameTextColor); + ImGui.SetCursorPos(minCoord with { Y = ImGui.GetCursorPosY() }); + ImGui.TextUnformatted(this.InitiatorString); + ImGui.PopStyleColor(); + + ImGui.PopTextWrapPos(); + return ImGui.GetCursorPosY() - minCoord.Y; + } + + private void DrawContentBody(Vector2 minCoord, float width) + { + ImGui.SetCursorPos(minCoord); + ImGui.PushTextWrapPos(minCoord.X + width); + ImGui.PushStyleColor(ImGuiCol.Text, NotificationConstants.BodyTextColor); + ImGui.TextUnformatted(this.Content); + ImGui.PopStyleColor(); + ImGui.PopTextWrapPos(); + } + + private void DrawExpiryBar(DateTime effectiveExpiry, bool warrantsExtension) + { + float barL, barR; + if (this.DismissReason is not null) + { + var v = this.hideEasing.IsDone ? 0f : 1f - (float)this.hideEasing.Value; + var midpoint = (this.prevProgressL + this.prevProgressR) / 2f; + var length = (this.prevProgressR - this.prevProgressL) / 2f; + barL = midpoint - (length * v); + barR = midpoint + (length * v); + } + else if (warrantsExtension) + { + barL = 0f; + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else if (effectiveExpiry == DateTime.MaxValue) + { + if (this.ShowIndeterminateIfNoExpiry) + { + var elapsed = (float)(((DateTime.Now - this.CreatedAt).TotalMilliseconds % + NotificationConstants.IndeterminateProgressbarLoopDuration) / + NotificationConstants.IndeterminateProgressbarLoopDuration); + barL = Math.Max(elapsed - (1f / 3), 0f) / (2f / 3); + barR = Math.Min(elapsed, 2f / 3) / (2f / 3); + barL = MathF.Pow(barL, 3); + barR = 1f - MathF.Pow(1f - barR, 3); + this.prevProgressL = barL; + this.prevProgressR = barR; + } + else + { + this.prevProgressL = barL = 0f; + this.prevProgressR = barR = 1f; + } + } + else + { + barL = 1f - (float)((effectiveExpiry - DateTime.Now).TotalMilliseconds / + (effectiveExpiry - this.lastInterestTime).TotalMilliseconds); + barR = 1f; + this.prevProgressL = barL; + this.prevProgressR = barR; + } + + barR = Math.Clamp(barR, 0f, 1f); + + var windowPos = ImGui.GetWindowPos(); + var windowSize = ImGui.GetWindowSize(); + ImGui.PushClipRect(windowPos, windowPos + windowSize, false); + ImGui.GetWindowDrawList().AddRectFilled( + windowPos + new Vector2( + windowSize.X * barL, + windowSize.Y - NotificationConstants.ScaledExpiryProgressBarHeight), + windowPos + windowSize with { X = windowSize.X * barR }, + ImGui.GetColorU32(this.Type.ToColor())); + ImGui.PopClipRect(); + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs new file mode 100644 index 000000000..3bc7c3837 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/ActiveNotification.cs @@ -0,0 +1,370 @@ +using System.Runtime.Loader; +using System.Threading; +using System.Threading.Tasks; + +using Dalamud.Interface.Animation; +using Dalamud.Interface.Animation.EasingFunctions; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Utility; + +using Serilog; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Represents an active notification. +internal sealed partial class ActiveNotification : IActiveNotification +{ + private readonly Notification underlyingNotification; + + private readonly Easing showEasing; + private readonly Easing hideEasing; + private readonly Easing progressEasing; + private readonly Easing expandoEasing; + + /// Gets the time of starting to count the timer for the expiration. + private DateTime lastInterestTime; + + /// Gets the extended expiration time from . + private DateTime extendedExpiry; + + /// The icon texture to use if specified; otherwise, icon will be used from . + private Task? iconTextureWrap; + + /// The plugin that initiated this notification. + private LocalPlugin? initiatorPlugin; + + /// Whether has been unloaded. + private bool isInitiatorUnloaded; + + /// The progress before for the progress bar animation with . + private float progressBefore; + + /// Used for calculating correct dismissal progressbar animation (left edge). + private float prevProgressL; + + /// Used for calculating correct dismissal progressbar animation (right edge). + private float prevProgressR; + + /// New progress value to be updated on next call to . + private float? newProgress; + + /// New minimized value to be updated on next call to . + private bool? newMinimized; + + /// Initializes a new instance of the class. + /// The underlying notification. + /// The initiator plugin. Use null if originated by Dalamud. + public ActiveNotification(Notification underlyingNotification, LocalPlugin? initiatorPlugin) + { + this.underlyingNotification = underlyingNotification with { }; + this.initiatorPlugin = initiatorPlugin; + this.showEasing = new InCubic(NotificationConstants.ShowAnimationDuration); + this.hideEasing = new OutCubic(NotificationConstants.HideAnimationDuration); + this.progressEasing = new InOutCubic(NotificationConstants.ProgressChangeAnimationDuration); + this.expandoEasing = new InOutCubic(NotificationConstants.ExpandoAnimationDuration); + this.CreatedAt = this.lastInterestTime = this.extendedExpiry = DateTime.Now; + + this.showEasing.Start(); + this.progressEasing.Start(); + } + + /// + public long Id { get; } = IActiveNotification.CreateNewId(); + + /// + public DateTime CreatedAt { get; } + + /// + public string Content + { + get => this.underlyingNotification.Content; + set => this.underlyingNotification.Content = value; + } + + /// + public string? Title + { + get => this.underlyingNotification.Title; + set => this.underlyingNotification.Title = value; + } + + /// + public bool RespectUiHidden + { + get => this.underlyingNotification.RespectUiHidden; + set => this.underlyingNotification.RespectUiHidden = value; + } + + /// + public string? MinimizedText + { + get => this.underlyingNotification.MinimizedText; + set => this.underlyingNotification.MinimizedText = value; + } + + /// + public NotificationType Type + { + get => this.underlyingNotification.Type; + set => this.underlyingNotification.Type = value; + } + + /// + public INotificationIcon? Icon + { + get => this.underlyingNotification.Icon; + set => this.underlyingNotification.Icon = value; + } + + /// + public DateTime HardExpiry + { + get => this.underlyingNotification.HardExpiry; + set + { + if (this.underlyingNotification.HardExpiry == value) + return; + this.underlyingNotification.HardExpiry = value; + this.lastInterestTime = DateTime.Now; + } + } + + /// + public TimeSpan InitialDuration + { + get => this.underlyingNotification.InitialDuration; + set + { + this.underlyingNotification.InitialDuration = value; + this.lastInterestTime = DateTime.Now; + } + } + + /// + public TimeSpan ExtensionDurationSinceLastInterest + { + get => this.underlyingNotification.ExtensionDurationSinceLastInterest; + set + { + this.underlyingNotification.ExtensionDurationSinceLastInterest = value; + this.lastInterestTime = DateTime.Now; + } + } + + /// + public DateTime EffectiveExpiry { get; private set; } + + /// + public NotificationDismissReason? DismissReason { get; private set; } + + /// + public bool ShowIndeterminateIfNoExpiry + { + get => this.underlyingNotification.ShowIndeterminateIfNoExpiry; + set => this.underlyingNotification.ShowIndeterminateIfNoExpiry = value; + } + + /// + public bool Minimized + { + get => this.newMinimized ?? this.underlyingNotification.Minimized; + set => this.newMinimized = value; + } + + /// + public bool UserDismissable + { + get => this.underlyingNotification.UserDismissable; + set => this.underlyingNotification.UserDismissable = value; + } + + /// + public float Progress + { + get => this.newProgress ?? this.underlyingNotification.Progress; + set => this.newProgress = value; + } + + /// Gets the eased progress. + private float ProgressEased + { + get + { + var underlyingProgress = this.underlyingNotification.Progress; + if (Math.Abs(underlyingProgress - this.progressBefore) < 0.000001f || this.progressEasing.IsDone) + return underlyingProgress; + + var state = Math.Clamp((float)this.progressEasing.Value, 0f, 1f); + return this.progressBefore + (state * (underlyingProgress - this.progressBefore)); + } + } + + /// Gets the string for the initiator field. + private string InitiatorString => + this.initiatorPlugin is not { } plugin + ? NotificationConstants.DefaultInitiator + : this.isInitiatorUnloaded + ? NotificationConstants.UnloadedInitiatorNameFormat.Format(plugin.Name) + : plugin.Name; + + /// Gets the effective text to display when minimized. + private string EffectiveMinimizedText => (this.MinimizedText ?? this.Content).ReplaceLineEndings(" "); + + /// + public void DismissNow() => this.DismissNow(NotificationDismissReason.Programmatical); + + /// Dismisses this notification. Multiple calls will be ignored. + /// The reason of dismissal. + public void DismissNow(NotificationDismissReason reason) + { + if (this.DismissReason is not null) + return; + + this.DismissReason = reason; + this.hideEasing.Start(); + this.InvokeDismiss(); + } + + /// + public void ExtendBy(TimeSpan extension) + { + var newExpiry = DateTime.Now + extension; + if (this.extendedExpiry < newExpiry) + this.extendedExpiry = newExpiry; + } + + /// + public void SetIconTexture(IDalamudTextureWrap? textureWrap) + { + this.SetIconTexture(textureWrap is null ? null : Task.FromResult(textureWrap)); + } + + /// + public void SetIconTexture(Task? textureWrapTask) + { + if (this.DismissReason is not null) + { + textureWrapTask?.ToContentDisposedTask(true); + return; + } + + // After replacing, if the old texture is not the old texture, then dispose the old texture. + if (Interlocked.Exchange(ref this.iconTextureWrap, textureWrapTask) is { } wrapTaskToDispose && + wrapTaskToDispose != textureWrapTask) + { + wrapTaskToDispose.ToContentDisposedTask(true); + } + } + + /// Removes non-Dalamud invocation targets from events. + /// + /// This is done to prevent references of plugins being unloaded from outliving the plugin itself. + /// Anything that can contain plugin-provided types and functions count, which effectively means that events and + /// interface/object-typed fields need to be scrubbed. + /// As a notification can be marked as non-user-dismissable, in which case after removing event handlers there will + /// be no way to remove the notification, we force the notification to become user-dismissable, and reset the expiry + /// to the default duration on unload. + /// + internal void RemoveNonDalamudInvocations() + { + var dalamudContext = AssemblyLoadContext.GetLoadContext(typeof(NotificationManager).Assembly); + this.Dismiss = RemoveNonDalamudInvocationsCore(this.Dismiss); + this.Click = RemoveNonDalamudInvocationsCore(this.Click); + this.DrawActions = RemoveNonDalamudInvocationsCore(this.DrawActions); + + if (this.Icon is { } previousIcon && !IsOwnedByDalamud(previousIcon.GetType())) + this.Icon = null; + + this.isInitiatorUnloaded = true; + this.UserDismissable = true; + this.ExtensionDurationSinceLastInterest = NotificationConstants.DefaultDuration; + + var newMaxExpiry = DateTime.Now + NotificationConstants.DefaultDuration; + if (this.EffectiveExpiry > newMaxExpiry) + this.HardExpiry = newMaxExpiry; + + return; + + bool IsOwnedByDalamud(Type t) => AssemblyLoadContext.GetLoadContext(t.Assembly) == dalamudContext; + + T? RemoveNonDalamudInvocationsCore(T? @delegate) where T : Delegate + { + if (@delegate is null) + return null; + + foreach (var il in @delegate.GetInvocationList()) + { + if (il.Target is { } target && !IsOwnedByDalamud(target.GetType())) + @delegate = (T)Delegate.Remove(@delegate, il); + } + + return @delegate; + } + } + + /// Updates the state of this notification, and release the relevant resource if this notification is no + /// longer in use. + /// true if the notification is over and relevant resources are released. + /// Intended to be called from the main thread only. + internal bool UpdateOrDisposeInternal() + { + this.showEasing.Update(); + this.hideEasing.Update(); + this.progressEasing.Update(); + if (this.expandoEasing.IsRunning) + { + this.expandoEasing.Update(); + if (this.expandoEasing.IsDone) + this.expandoEasing.Stop(); + } + + if (this.newProgress is { } newProgressValue) + { + if (Math.Abs(this.underlyingNotification.Progress - newProgressValue) > float.Epsilon) + { + this.progressBefore = this.ProgressEased; + this.underlyingNotification.Progress = newProgressValue; + this.progressEasing.Restart(); + this.progressEasing.Update(); + } + + this.newProgress = null; + } + + if (this.newMinimized is { } newMinimizedValue) + { + if (this.underlyingNotification.Minimized != newMinimizedValue) + { + this.underlyingNotification.Minimized = newMinimizedValue; + this.expandoEasing.Restart(); + this.expandoEasing.Update(); + } + + this.newMinimized = null; + } + + if (!this.hideEasing.IsRunning || !this.hideEasing.IsDone) + return false; + + this.DisposeInternal(); + return true; + } + + /// Clears the resources associated with this instance of . + internal void DisposeInternal() + { + if (Interlocked.Exchange(ref this.iconTextureWrap, null) is { } wrapTaskToDispose) + wrapTaskToDispose.ToContentDisposedTask(true); + this.Dismiss = null; + this.Click = null; + this.DrawActions = null; + this.initiatorPlugin = null; + } + + private void LogEventInvokeError(Exception exception, string message) => + Log.Error( + exception, + $"[{nameof(ActiveNotification)}:{this.initiatorPlugin?.Name ?? NotificationConstants.DefaultInitiator}] {message}"); +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs new file mode 100644 index 000000000..de212160c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationConstants.cs @@ -0,0 +1,161 @@ +using System.Numerics; + +using CheapLoc; + +using Dalamud.Interface.Colors; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.Utility; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Constants for drawing notification windows. +internal static class NotificationConstants +{ + // .............................[..] + // ..when.......................[XX] + // .. .. + // ..[i]..title title title title .. + // .. by this_plugin .. + // .. .. + // .. body body body body .. + // .. some more wrapped body .. + // .. .. + // .. action buttons .. + // ................................. + + /// The string to measure size of, to decide the width of notification windows. + /// Probably not worth localizing. + public const string NotificationWidthMeasurementString = + "The width of this text will decide the width\n" + + "of the notification window."; + + /// The ratio of maximum notification window width w.r.t. main viewport width. + public const float MaxNotificationWindowWidthWrtMainViewportWidth = 2f / 3; + + /// The size of the icon. + public const float IconSize = 32; + + /// The background opacity of a notification window. + public const float BackgroundOpacity = 0.82f; + + /// The duration of indeterminate progress bar loop in milliseconds. + public const float IndeterminateProgressbarLoopDuration = 2000f; + + /// The duration of the progress wave animation in milliseconds. + public const float ProgressWaveLoopDuration = 2000f; + + /// The time ratio of a progress wave loop where the animation is idle. + public const float ProgressWaveIdleTimeRatio = 0.5f; + + /// The time ratio of a non-idle portion of the progress wave loop where the color is the most opaque. + /// + public const float ProgressWaveLoopMaxColorTimeRatio = 0.7f; + + /// Default duration of the notification. + public static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(3); + + /// Duration of show animation. + public static readonly TimeSpan ShowAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Duration of hide animation. + public static readonly TimeSpan HideAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Duration of progress change animation. + public static readonly TimeSpan ProgressChangeAnimationDuration = TimeSpan.FromMilliseconds(200); + + /// Duration of expando animation. + public static readonly TimeSpan ExpandoAnimationDuration = TimeSpan.FromMilliseconds(300); + + /// Text color for the rectangular border when the notification is focused. + public static readonly Vector4 FocusBorderColor = new(0.4f, 0.4f, 0.4f, 1f); + + /// Text color for the when. + public static readonly Vector4 WhenTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the close button [X]. + public static readonly Vector4 CloseTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the title. + public static readonly Vector4 TitleTextColor = new(1f, 1f, 1f, 1f); + + /// Text color for the name of the initiator. + public static readonly Vector4 BlameTextColor = new(0.8f, 0.8f, 0.8f, 1f); + + /// Text color for the body. + public static readonly Vector4 BodyTextColor = new(0.9f, 0.9f, 0.9f, 1f); + + /// Color for the background progress bar (determinate progress only). + public static readonly Vector4 BackgroundProgressColorMax = new(1f, 1f, 1f, 0.1f); + + /// Color for the background progress bar (determinate progress only). + public static readonly Vector4 BackgroundProgressColorMin = new(1f, 1f, 1f, 0.05f); + + /// Gets the scaled padding of the window (dot(.) in the above diagram). + public static float ScaledWindowPadding => MathF.Round(16 * ImGuiHelpers.GlobalScale); + + /// Gets the distance from the right bottom border of the viewport + /// to the right bottom border of a notification window. + /// + public static float ScaledViewportEdgeMargin => MathF.Round(20 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between two notification windows. + public static float ScaledWindowGap => MathF.Round(10 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled gap between components. + public static float ScaledComponentGap => MathF.Round(5 * ImGuiHelpers.GlobalScale); + + /// Gets the scaled size of the icon. + public static float ScaledIconSize => MathF.Round(IconSize * ImGuiHelpers.GlobalScale); + + /// Gets the height of the expiry progress bar. + public static float ScaledExpiryProgressBarHeight => MathF.Round(3 * ImGuiHelpers.GlobalScale); + + /// Gets the thickness of the focus indicator rectangle. + public static float FocusIndicatorThickness => MathF.Round(3 * ImGuiHelpers.GlobalScale); + + /// Gets the string to show in place of this_plugin if the notification is shown by Dalamud. + public static string DefaultInitiator => Loc.Localize("NotificationConstants.DefaultInitiator", "Dalamud"); + + /// Gets the string format of the initiator name field, if the initiator is unloaded. + public static string UnloadedInitiatorNameFormat => + Loc.Localize("NotificationConstants.UnloadedInitiatorNameFormat", "{0} (unloaded)"); + + /// Gets the color corresponding to the notification type. + /// The notification type. + /// The corresponding color. + public static Vector4 ToColor(this NotificationType type) => type switch + { + NotificationType.None => ImGuiColors.DalamudWhite, + NotificationType.Success => ImGuiColors.HealerGreen, + NotificationType.Warning => ImGuiColors.DalamudOrange, + NotificationType.Error => ImGuiColors.DalamudRed, + NotificationType.Info => ImGuiColors.TankBlue, + _ => ImGuiColors.DalamudWhite, + }; + + /// Gets the char value corresponding to the notification type. + /// The notification type. + /// The corresponding char, or null. + public static char ToChar(this NotificationType type) => type switch + { + NotificationType.None => '\0', + NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconChar(), + NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconChar(), + NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconChar(), + NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconChar(), + _ => '\0', + }; + + /// Gets the localized title string corresponding to the notification type. + /// The notification type. + /// The corresponding title. + public static string? ToTitle(this NotificationType type) => type switch + { + NotificationType.None => null, + NotificationType.Success => Loc.Localize("NotificationConstants.Title.Success", "Success"), + NotificationType.Warning => Loc.Localize("NotificationConstants.Title.Warning", "Warning"), + NotificationType.Error => Loc.Localize("NotificationConstants.Title.Error", "Error"), + NotificationType.Info => Loc.Localize("NotificationConstants.Title.Info", "Info"), + _ => null, + }; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs new file mode 100644 index 000000000..3aa712160 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FilePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.IO; +using System.Numerics; + +using Dalamud.Interface.Internal; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a texture from a file as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class FilePathNotificationIcon : INotificationIcon +{ + private readonly FileInfo fileInfo; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + public FilePathNotificationIcon(string filePath) => this.fileInfo = new(filePath); + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromFile(this.fileInfo)); + + /// + public override bool Equals(object? obj) => + obj is FilePathNotificationIcon r && r.fileInfo.FullName == this.fileInfo.FullName; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.fileInfo.FullName); + + /// + public override string ToString() => $"{nameof(FilePathNotificationIcon)}({this.fileInfo.FullName})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs new file mode 100644 index 000000000..0acfdee4c --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/FontAwesomeIconNotificationIcon.cs @@ -0,0 +1,31 @@ +using System.Numerics; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class FontAwesomeIconNotificationIcon : INotificationIcon +{ + private readonly char iconChar; + + /// Initializes a new instance of the class. + /// The character. + public FontAwesomeIconNotificationIcon(FontAwesomeIcon iconChar) => this.iconChar = (char)iconChar; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + this.iconChar, + Service.Get().IconFontAwesomeFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is FontAwesomeIconNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(FontAwesomeIconNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs new file mode 100644 index 000000000..e0699e1b6 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/GamePathNotificationIcon.cs @@ -0,0 +1,34 @@ +using System.Numerics; + +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of a game-shipped texture as the icon of a notification. +/// If there was no texture loaded for any reason, the plugin icon will be displayed instead. +internal class GamePathNotificationIcon : INotificationIcon +{ + private readonly string gamePath; + + /// Initializes a new instance of the class. + /// The path to a .tex file inside the game resources. + /// Use to get the game path from icon IDs. + public GamePathNotificationIcon(string gamePath) => this.gamePath = gamePath; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + Service.Get().GetTextureFromGame(this.gamePath)); + + /// + public override bool Equals(object? obj) => obj is GamePathNotificationIcon r && r.gamePath == this.gamePath; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.gamePath); + + /// + public override string ToString() => $"{nameof(GamePathNotificationIcon)}({this.gamePath})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs new file mode 100644 index 000000000..3bbd8dd81 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationIcon/SeIconCharNotificationIcon.cs @@ -0,0 +1,33 @@ +using System.Numerics; + +using Dalamud.Game.Text; + +namespace Dalamud.Interface.ImGuiNotification.Internal.NotificationIcon; + +/// Represents the use of as the icon of a notification. +internal class SeIconCharNotificationIcon : INotificationIcon +{ + private readonly SeIconChar iconChar; + + /// Initializes a new instance of the class. + /// The character. + public SeIconCharNotificationIcon(SeIconChar c) => this.iconChar = c; + + /// + public bool DrawIcon(Vector2 minCoord, Vector2 maxCoord, Vector4 color) => + NotificationUtilities.DrawIconFrom( + minCoord, + maxCoord, + (char)this.iconChar, + Service.Get().IconAxisFontHandle, + color); + + /// + public override bool Equals(object? obj) => obj is SeIconCharNotificationIcon r && r.iconChar == this.iconChar; + + /// + public override int GetHashCode() => HashCode.Combine(this.GetType().GetHashCode(), this.iconChar); + + /// + public override string ToString() => $"{nameof(SeIconCharNotificationIcon)}({this.iconChar})"; +} diff --git a/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs new file mode 100644 index 000000000..272407615 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Internal/NotificationManager.cs @@ -0,0 +1,165 @@ +using System.Collections.Concurrent; +using System.Collections.Generic; + +using Dalamud.Game.Gui; +using Dalamud.Interface.GameFonts; +using Dalamud.Interface.Internal.Notifications; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Interface.Utility; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification.Internal; + +/// Class handling notifications/toasts in ImGui. +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +internal class NotificationManager : INotificationManager, IServiceType, IDisposable +{ + [ServiceManager.ServiceDependency] + private readonly GameGui gameGui = Service.Get(); + + private readonly List notifications = new(); + private readonly ConcurrentBag pendingNotifications = new(); + + [ServiceManager.ServiceConstructor] + private NotificationManager(FontAtlasFactory fontAtlasFactory) + { + this.PrivateAtlas = fontAtlasFactory.CreateFontAtlas( + nameof(NotificationManager), + FontAtlasAutoRebuildMode.Async); + this.IconAxisFontHandle = + this.PrivateAtlas.NewGameFontHandle(new(GameFontFamily.Axis, NotificationConstants.IconSize)); + this.IconFontAwesomeFontHandle = + this.PrivateAtlas.NewDelegateFontHandle( + e => e.OnPreBuild( + tk => tk.AddFontAwesomeIconFont(new() { SizePx = NotificationConstants.IconSize }))); + } + + /// Gets the handle to AXIS fonts, sized for use as an icon. + public IFontHandle IconAxisFontHandle { get; } + + /// Gets the handle to FontAwesome fonts, sized for use as an icon. + public IFontHandle IconFontAwesomeFontHandle { get; } + + /// Gets the private atlas for use with notification windows. + private IFontAtlas PrivateAtlas { get; } + + /// + public void Dispose() + { + this.PrivateAtlas.Dispose(); + foreach (var n in this.pendingNotifications) + n.DisposeInternal(); + foreach (var n in this.notifications) + n.DisposeInternal(); + this.pendingNotifications.Clear(); + this.notifications.Clear(); + } + + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = new ActiveNotification(notification, null); + this.pendingNotifications.Add(an); + return an; + } + + /// Adds a notification originating from a plugin. + /// The notification. + /// The source plugin. + /// The added notification. + public IActiveNotification AddNotification(Notification notification, LocalPlugin plugin) + { + var an = new ActiveNotification(notification, plugin); + this.pendingNotifications.Add(an); + return an; + } + + /// Add a notification to the notification queue. + /// The content of the notification. + /// The title of the notification. + /// The type of the notification. + public void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None) => + this.AddNotification( + new() + { + Content = content, + Title = title, + Type = type, + }); + + /// Draw all currently queued notifications. + public void Draw() + { + var viewportSize = ImGuiHelpers.MainViewport.WorkSize; + var height = 0f; + var uiHidden = this.gameGui.GameUiHidden; + + while (this.pendingNotifications.TryTake(out var newNotification)) + this.notifications.Add(newNotification); + + var width = ImGui.CalcTextSize(NotificationConstants.NotificationWidthMeasurementString).X; + width += NotificationConstants.ScaledWindowPadding * 3; + width += NotificationConstants.ScaledIconSize; + width = Math.Min(width, viewportSize.X * NotificationConstants.MaxNotificationWindowWidthWrtMainViewportWidth); + + this.notifications.RemoveAll(static x => x.UpdateOrDisposeInternal()); + foreach (var tn in this.notifications) + { + if (uiHidden && tn.RespectUiHidden) + continue; + height += tn.Draw(width, height) + NotificationConstants.ScaledWindowGap; + } + } +} + +/// Plugin-scoped version of a service. +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class NotificationManagerPluginScoped : INotificationManager, IServiceType, IDisposable +{ + private readonly LocalPlugin localPlugin; + private readonly ConcurrentDictionary notifications = new(); + + [ServiceManager.ServiceDependency] + private readonly NotificationManager notificationManagerService = Service.Get(); + + [ServiceManager.ServiceConstructor] + private NotificationManagerPluginScoped(LocalPlugin localPlugin) => + this.localPlugin = localPlugin; + + /// + public IActiveNotification AddNotification(Notification notification) + { + var an = this.notificationManagerService.AddNotification(notification, this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); + return an; + } + + /// + public void Dispose() + { + while (!this.notifications.IsEmpty) + { + foreach (var n in this.notifications.Keys) + { + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); + } + } + } +} diff --git a/Dalamud/Interface/ImGuiNotification/Notification.cs b/Dalamud/Interface/ImGuiNotification/Notification.cs new file mode 100644 index 000000000..5175985c7 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/Notification.cs @@ -0,0 +1,52 @@ +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.Notifications; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Represents a blueprint for a notification. +public sealed record Notification : INotification +{ + /// + /// Gets the default value for and . + /// + public static TimeSpan DefaultDuration => NotificationConstants.DefaultDuration; + + /// + public string Content { get; set; } = string.Empty; + + /// + public string? Title { get; set; } + + /// + public string? MinimizedText { get; set; } + + /// + public NotificationType Type { get; set; } = NotificationType.None; + + /// + public INotificationIcon? Icon { get; set; } + + /// + public DateTime HardExpiry { get; set; } = DateTime.MaxValue; + + /// + public TimeSpan InitialDuration { get; set; } = DefaultDuration; + + /// + public TimeSpan ExtensionDurationSinceLastInterest { get; set; } = DefaultDuration; + + /// + public bool ShowIndeterminateIfNoExpiry { get; set; } = true; + + /// + public bool RespectUiHidden { get; set; } = true; + + /// + public bool Minimized { get; set; } = true; + + /// + public bool UserDismissable { get; set; } = true; + + /// + public float Progress { get; set; } = 1f; +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs new file mode 100644 index 000000000..2c9d6d2a4 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationDismissReason.cs @@ -0,0 +1,16 @@ +namespace Dalamud.Interface.ImGuiNotification; + +/// Specifies the reason of dismissal for a notification. +public enum NotificationDismissReason +{ + /// The notification is dismissed because the expiry specified from is + /// met. + Timeout = 1, + + /// The notification is dismissed because the user clicked on the close button on a notification window. + /// + Manual = 2, + + /// The notification is dismissed from calling . + Programmatical = 3, +} diff --git a/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs new file mode 100644 index 000000000..631263f95 --- /dev/null +++ b/Dalamud/Interface/ImGuiNotification/NotificationUtilities.cs @@ -0,0 +1,149 @@ +using System.IO; +using System.Numerics; +using System.Runtime.CompilerServices; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.Internal; +using Dalamud.Interface.Internal.Windows; +using Dalamud.Interface.ManagedFontAtlas; +using Dalamud.Interface.Utility; +using Dalamud.Plugin.Internal.Types; +using Dalamud.Storage.Assets; + +using ImGuiNET; + +namespace Dalamud.Interface.ImGuiNotification; + +/// Utilities for implementing stuff under . +public static class NotificationUtilities +{ + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon ToNotificationIcon(this SeIconChar iconChar) => + INotificationIcon.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon ToNotificationIcon(this FontAwesomeIcon iconChar) => + INotificationIcon.From(iconChar); + + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static INotificationIcon ToNotificationIcon(this FileInfo fileInfo) => + INotificationIcon.FromFile(fileInfo.FullName); + + /// Draws an icon from an and a . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The icon character. + /// The font handle to use. + /// The foreground color. + /// true if anything has been drawn. + internal static unsafe bool DrawIconFrom( + Vector2 minCoord, + Vector2 maxCoord, + char c, + IFontHandle fontHandle, + Vector4 color) + { + if (c is '\0' or char.MaxValue) + return false; + + var smallerDim = Math.Max(maxCoord.Y - minCoord.Y, maxCoord.X - minCoord.X); + using (fontHandle.Push()) + { + var font = ImGui.GetFont(); + var glyphPtr = (ImGuiHelpers.ImFontGlyphReal*)font.FindGlyphNoFallback(c).NativePtr; + if (glyphPtr is null) + return false; + + ref readonly var glyph = ref *glyphPtr; + var size = glyph.XY1 - glyph.XY0; + var smallerSizeDim = Math.Min(size.X, size.Y); + var scale = smallerSizeDim > smallerDim ? smallerDim / smallerSizeDim : 1f; + size *= scale; + var pos = ((minCoord + maxCoord) - size) / 2; + pos += ImGui.GetWindowPos(); + ImGui.GetWindowDrawList().AddImage( + font.ContainerAtlas.Textures[glyph.TextureIndex].TexID, + pos, + pos + size, + glyph.UV0, + glyph.UV1, + ImGui.GetColorU32(color with { W = color.W * ImGui.GetStyle().Alpha })); + } + + return true; + } + + /// Draws an icon from an instance of . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The texture. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, IDalamudTextureWrap? texture) + { + if (texture is null) + return false; + try + { + var handle = texture.ImGuiHandle; + var size = texture.Size; + if (size.X > maxCoord.X - minCoord.X) + size *= (maxCoord.X - minCoord.X) / size.X; + if (size.Y > maxCoord.Y - minCoord.Y) + size *= (maxCoord.Y - minCoord.Y) / size.Y; + ImGui.SetCursorPos(((minCoord + maxCoord) - size) / 2); + ImGui.Image(handle, size); + return true; + } + catch + { + return false; + } + } + + /// Draws an icon from an instance of that results in an + /// . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The task that results in a texture. + /// true if anything has been drawn. + /// Exceptions from the task will be treated as if no texture is provided. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, Task? textureTask) => + textureTask?.IsCompletedSuccessfully is true && DrawIconFrom(minCoord, maxCoord, textureTask.Result); + + /// Draws an icon from an instance of . + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + /// The plugin. Dalamud icon will be drawn if null is given. + /// true if anything has been drawn. + internal static bool DrawIconFrom(Vector2 minCoord, Vector2 maxCoord, LocalPlugin? plugin) + { + var dam = Service.Get(); + if (plugin is null) + return false; + + if (!Service.Get().TryGetIcon( + plugin, + plugin.Manifest, + plugin.IsThirdParty, + out var texture) || texture is null) + { + texture = dam.GetDalamudTextureWrap(DalamudAsset.DefaultIcon); + } + + return DrawIconFrom(minCoord, maxCoord, texture); + } + + /// Draws the Dalamud logo as an icon. + /// The coordinates of the top left of the icon area. + /// The coordinates of the bottom right of the icon area. + internal static void DrawIconFromDalamudLogo(Vector2 minCoord, Vector2 maxCoord) + { + var dam = Service.Get(); + var texture = dam.GetDalamudTextureWrap(DalamudAsset.LogoSmall); + DrawIconFrom(minCoord, maxCoord, texture); + } +} diff --git a/Dalamud/Interface/Internal/DalamudIme.cs b/Dalamud/Interface/Internal/DalamudIme.cs index caf014885..64040011e 100644 --- a/Dalamud/Interface/Internal/DalamudIme.cs +++ b/Dalamud/Interface/Internal/DalamudIme.cs @@ -34,7 +34,7 @@ namespace Dalamud.Interface.Internal; /// This class handles CJK IME. ///
[ServiceManager.EarlyLoadedService] -internal sealed unsafe class DalamudIme : IDisposable, IServiceType +internal sealed unsafe class DalamudIme : IInternalDisposableService { private const int CImGuiStbTextCreateUndoOffset = 0xB57A0; private const int CImGuiStbTextUndoOffset = 0xB59C0; @@ -200,7 +200,7 @@ internal sealed unsafe class DalamudIme : IDisposable, IServiceType this.candidateStrings.Count != 0 || this.ShowPartialConversion || this.inputModeIcon != default; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.Draw; this.ReleaseUnmanagedResources(); diff --git a/Dalamud/Interface/Internal/DalamudInterface.cs b/Dalamud/Interface/Internal/DalamudInterface.cs index 1a07cd6ae..ec18fbb69 100644 --- a/Dalamud/Interface/Internal/DalamudInterface.cs +++ b/Dalamud/Interface/Internal/DalamudInterface.cs @@ -46,7 +46,7 @@ namespace Dalamud.Interface.Internal; /// This plugin implements all of the Dalamud interface separately, to allow for reloading of the interface and rapid prototyping. ///
[ServiceManager.EarlyLoadedService] -internal class DalamudInterface : IDisposable, IServiceType +internal class DalamudInterface : IInternalDisposableService { private const float CreditsDarkeningMaxAlpha = 0.8f; @@ -209,7 +209,7 @@ internal class DalamudInterface : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.interfaceManager.Draw -= this.OnDraw; diff --git a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs index bbf665405..9fa21a31b 100644 --- a/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiClipboardFunctionProvider.cs @@ -35,7 +35,7 @@ namespace Dalamud.Interface.Internal; /// /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiClipboardFunctionProvider : IInternalDisposableService { private static readonly ModuleLog Log = new(nameof(ImGuiClipboardFunctionProvider)); private readonly nint clipboardUserDataOriginal; @@ -75,7 +75,7 @@ internal sealed unsafe class ImGuiClipboardFunctionProvider : IServiceType, IDis } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { if (!this.clipboardUserData.IsAllocated) return; diff --git a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs index f2d6ed244..139dd96e2 100644 --- a/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs +++ b/Dalamud/Interface/Internal/ImGuiDrawListFixProvider.cs @@ -24,7 +24,7 @@ namespace Dalamud.Interface.Internal; /// Change push_texture_id to only have one condition. /// [ServiceManager.EarlyLoadedService] -internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposable +internal sealed unsafe class ImGuiDrawListFixProvider : IInternalDisposableService { private const int CImGuiImDrawListAddPolyLineOffset = 0x589B0; private const int CImGuiImDrawListAddRectFilled = 0x59FD0; @@ -69,7 +69,7 @@ internal sealed unsafe class ImGuiDrawListFixProvider : IServiceType, IDisposabl ImDrawFlags flags); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.hookImDrawListAddPolyline.Dispose(); this.hookImDrawListAddRectFilled.Dispose(); diff --git a/Dalamud/Interface/Internal/InterfaceManager.cs b/Dalamud/Interface/Internal/InterfaceManager.cs index 126097ed3..be14b882b 100644 --- a/Dalamud/Interface/Internal/InterfaceManager.cs +++ b/Dalamud/Interface/Internal/InterfaceManager.cs @@ -5,6 +5,7 @@ using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; @@ -14,6 +15,7 @@ using Dalamud.Game.ClientState.Keys; using Dalamud.Game.Internal.DXGI; using Dalamud.Hooking; using Dalamud.Hooking.WndProcHook; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; @@ -50,7 +52,7 @@ namespace Dalamud.Interface.Internal; /// This class manages interaction with the ImGui interface. /// [ServiceManager.BlockingEarlyLoadedService] -internal class InterfaceManager : IDisposable, IServiceType +internal class InterfaceManager : IInternalDisposableService { /// /// The default font size, in points. @@ -68,10 +70,13 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceDependency] private readonly WndProcHookManager wndProcHookManager = Service.Get(); + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + private readonly SwapChainVtableResolver address = new(); - private readonly Hook setCursorHook; private RawDX11Scene? scene; + private Hook? setCursorHook; private Hook? presentHook; private Hook? resizeBuffersHook; @@ -86,8 +91,6 @@ internal class InterfaceManager : IDisposable, IServiceType [ServiceManager.ServiceConstructor] private InterfaceManager() { - this.setCursorHook = Hook.FromImport( - null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] @@ -232,25 +235,45 @@ internal class InterfaceManager : IDisposable, IServiceType /// /// Dispose of managed and unmanaged resources. /// - public void Dispose() + void IInternalDisposableService.DisposeService() { - if (Service.GetNullable() is { } framework) - framework.RunOnFrameworkThread(Disposer).Wait(); - else - Disposer(); + // Unload hooks from the framework thread if possible. + // We're currently off the framework thread, as this function can only be called from + // ServiceManager.UnloadAllServices, which is called from EntryPoint.RunThread. + // The functions being unhooked are mostly called from the main thread, so unhooking from the main thread when + // possible would avoid any chance of unhooking a function that currently is being called. + // If unloading is initiated from "Unload Dalamud" /xldev menu, then the framework would still be running, as + // Framework.Destroy has never been called and thus Framework.IsFrameworkUnloading cannot be true, and this + // function will actually run the destroy from the framework thread. + // Otherwise, as Framework.IsFrameworkUnloading should have been set, this code should run immediately. + this.framework.RunOnFrameworkThread(ClearHooks).Wait(); + + // Below this point, hooks are guaranteed to be no longer called. + + // A font resource lock outlives the parent handle and the owner atlas. It should be disposed. + Interlocked.Exchange(ref this.defaultFontResourceLock, null)?.Dispose(); + + // Font handles become invalid after disposing the atlas, but just to be safe. + this.DefaultFontHandle?.Dispose(); + this.DefaultFontHandle = null; + + this.MonoFontHandle?.Dispose(); + this.MonoFontHandle = null; + + this.IconFontHandle?.Dispose(); + this.IconFontHandle = null; + + Interlocked.Exchange(ref this.dalamudAtlas, null)?.Dispose(); + Interlocked.Exchange(ref this.scene, null)?.Dispose(); - this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; - this.defaultFontResourceLock?.Dispose(); // lock outlives handle and atlas - this.defaultFontResourceLock = null; - this.dalamudAtlas?.Dispose(); - this.scene?.Dispose(); return; - void Disposer() + void ClearHooks() { - this.setCursorHook.Dispose(); - this.presentHook?.Dispose(); - this.resizeBuffersHook?.Dispose(); + this.wndProcHookManager.PreWndProc -= this.WndProcHookManagerOnPreWndProc; + Interlocked.Exchange(ref this.setCursorHook, null)?.Dispose(); + Interlocked.Exchange(ref this.presentHook, null)?.Dispose(); + Interlocked.Exchange(ref this.resizeBuffersHook, null)?.Dispose(); } } @@ -692,7 +715,6 @@ internal class InterfaceManager : IDisposable, IServiceType "InterfaceManager accepts event registration and stuff even when the game window is not ready.")] private void ContinueConstruction( TargetSigScanner sigScanner, - Framework framework, FontAtlasFactory fontAtlasFactory) { this.dalamudAtlas = fontAtlasFactory @@ -730,7 +752,7 @@ internal class InterfaceManager : IDisposable, IServiceType this.DefaultFontHandle.ImFontChanged += (_, font) => { var fontLocked = font.NewRef(); - Service.Get().RunOnFrameworkThread( + this.framework.RunOnFrameworkThread( () => { // Update the ImGui default font. @@ -764,6 +786,7 @@ internal class InterfaceManager : IDisposable, IServiceType Log.Error(ex, "Could not enable immersive mode"); } + this.setCursorHook = Hook.FromImport(null, "user32.dll", "SetCursor", 0, this.SetCursorDetour); this.presentHook = Hook.FromAddress(this.address.Present, this.PresentDetour); this.resizeBuffersHook = Hook.FromAddress(this.address.ResizeBuffers, this.ResizeBuffersDetour); @@ -807,7 +830,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.lastWantCapture && (!this.scene?.IsImGuiCursor(hCursor) ?? false) && this.OverrideGameCursor) return IntPtr.Zero; - return this.setCursorHook.IsDisposed + return this.setCursorHook?.IsDisposed is not false ? User32.SetCursor(new(hCursor, false)).DangerousGetHandle() : this.setCursorHook.Original(hCursor); } @@ -917,7 +940,7 @@ internal class InterfaceManager : IDisposable, IServiceType if (this.IsDispatchingEvents) { this.Draw?.Invoke(); - Service.Get().Draw(); + Service.GetNullable()?.Draw(); } ImGuiManagedAsserts.ReportProblems("Dalamud Core", snap); diff --git a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs b/Dalamud/Interface/Internal/Notifications/NotificationManager.cs deleted file mode 100644 index 67ad3ee8f..000000000 --- a/Dalamud/Interface/Internal/Notifications/NotificationManager.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Numerics; - -using Dalamud.Interface.Colors; -using Dalamud.Interface.Utility; -using Dalamud.Utility; -using ImGuiNET; - -namespace Dalamud.Interface.Internal.Notifications; - -/// -/// Class handling notifications/toasts in ImGui. -/// Ported from https://github.com/patrickcjk/imgui-notify. -/// -[ServiceManager.EarlyLoadedService] -internal class NotificationManager : IServiceType -{ - /// - /// Value indicating the bottom-left X padding. - /// - internal const float NotifyPaddingX = 20.0f; - - /// - /// Value indicating the bottom-left Y padding. - /// - internal const float NotifyPaddingY = 20.0f; - - /// - /// Value indicating the Y padding between each message. - /// - internal const float NotifyPaddingMessageY = 10.0f; - - /// - /// Value indicating the fade-in and out duration. - /// - internal const int NotifyFadeInOutTime = 500; - - /// - /// Value indicating the default time until the notification is dismissed. - /// - internal const int NotifyDefaultDismiss = 3000; - - /// - /// Value indicating the maximum opacity. - /// - internal const float NotifyOpacity = 0.82f; - - /// - /// Value indicating default window flags for the notifications. - /// - internal const ImGuiWindowFlags NotifyToastFlags = - ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoDecoration | ImGuiWindowFlags.NoInputs | - ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoBringToFrontOnFocus | ImGuiWindowFlags.NoFocusOnAppearing; - - private readonly List notifications = new(); - - [ServiceManager.ServiceConstructor] - private NotificationManager() - { - } - - /// - /// Add a notification to the notification queue. - /// - /// The content of the notification. - /// The title of the notification. - /// 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 = NotifyDefaultDismiss) - { - this.notifications.Add(new Notification - { - Content = content, - Title = title, - NotificationType = type, - DurationMs = msDelay, - }); - } - - /// - /// Draw all currently queued notifications. - /// - public void Draw() - { - var viewportSize = ImGuiHelpers.MainViewport.Size; - var height = 0f; - - for (var i = 0; i < this.notifications.Count; i++) - { - var tn = this.notifications.ElementAt(i); - - if (tn.GetPhase() == Notification.Phase.Expired) - { - this.notifications.RemoveAt(i); - continue; - } - - var opacity = tn.GetFadePercent(); - - var iconColor = tn.Color; - iconColor.W = opacity; - - var windowName = $"##NOTIFY{i}"; - - ImGuiHelpers.ForceNextWindowMainViewport(); - ImGui.SetNextWindowBgAlpha(opacity); - ImGui.SetNextWindowPos(ImGuiHelpers.MainViewport.Pos + new Vector2(viewportSize.X - NotifyPaddingX, viewportSize.Y - NotifyPaddingY - height), ImGuiCond.Always, Vector2.One); - ImGui.Begin(windowName, NotifyToastFlags); - - ImGui.PushTextWrapPos(viewportSize.X / 3.0f); - - var wasTitleRendered = false; - - if (!tn.Icon.IsNullOrEmpty()) - { - wasTitleRendered = true; - ImGui.PushFont(InterfaceManager.IconFont); - ImGui.TextColored(iconColor, tn.Icon); - ImGui.PopFont(); - } - - var textColor = ImGuiColors.DalamudWhite; - textColor.W = opacity; - - ImGui.PushStyleColor(ImGuiCol.Text, textColor); - - if (!tn.Title.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.Title); - wasTitleRendered = true; - } - else if (!tn.DefaultTitle.IsNullOrEmpty()) - { - if (!tn.Icon.IsNullOrEmpty()) - { - ImGui.SameLine(); - } - - ImGui.TextUnformatted(tn.DefaultTitle); - wasTitleRendered = true; - } - - if (wasTitleRendered && !tn.Content.IsNullOrEmpty()) - { - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 5.0f); - } - - if (!tn.Content.IsNullOrEmpty()) - { - if (wasTitleRendered) - { - ImGui.Separator(); - } - - ImGui.TextUnformatted(tn.Content); - } - - ImGui.PopStyleColor(); - - ImGui.PopTextWrapPos(); - - height += ImGui.GetWindowHeight() + NotifyPaddingMessageY; - - ImGui.End(); - } - } - - /// - /// Container class for notifications. - /// - internal class Notification - { - /// - /// Possible notification phases. - /// - internal enum Phase - { - /// - /// Phase indicating fade-in. - /// - FadeIn, - - /// - /// Phase indicating waiting until fade-out. - /// - Wait, - - /// - /// Phase indicating fade-out. - /// - FadeOut, - - /// - /// Phase indicating that the notification has expired. - /// - Expired, - } - - /// - /// Gets the type of the notification. - /// - internal NotificationType NotificationType { get; init; } - - /// - /// Gets the title of the notification. - /// - internal string? Title { get; init; } - - /// - /// Gets the content of the notification. - /// - internal string Content { get; init; } - - /// - /// Gets the duration of the notification in milliseconds. - /// - internal uint DurationMs { get; init; } - - /// - /// Gets the creation time of the notification. - /// - internal DateTime CreationTime { get; init; } = DateTime.Now; - - /// - /// Gets the default color of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal Vector4 Color => this.NotificationType switch - { - NotificationType.None => ImGuiColors.DalamudWhite, - NotificationType.Success => ImGuiColors.HealerGreen, - NotificationType.Warning => ImGuiColors.DalamudOrange, - NotificationType.Error => ImGuiColors.DalamudRed, - NotificationType.Info => ImGuiColors.TankBlue, - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the icon of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? Icon => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => FontAwesomeIcon.CheckCircle.ToIconString(), - NotificationType.Warning => FontAwesomeIcon.ExclamationCircle.ToIconString(), - NotificationType.Error => FontAwesomeIcon.TimesCircle.ToIconString(), - NotificationType.Info => FontAwesomeIcon.InfoCircle.ToIconString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the default title of the notification. - /// - /// Thrown when is set to an out-of-range value. - internal string? DefaultTitle => this.NotificationType switch - { - NotificationType.None => null, - NotificationType.Success => NotificationType.Success.ToString(), - NotificationType.Warning => NotificationType.Warning.ToString(), - NotificationType.Error => NotificationType.Error.ToString(), - NotificationType.Info => NotificationType.Info.ToString(), - _ => throw new ArgumentOutOfRangeException(), - }; - - /// - /// Gets the elapsed time since creating the notification. - /// - internal TimeSpan ElapsedTime => DateTime.Now - this.CreationTime; - - /// - /// Gets the phase of the notification. - /// - /// The phase of the notification. - internal Phase GetPhase() - { - var elapsed = (int)this.ElapsedTime.TotalMilliseconds; - - if (elapsed > NotifyFadeInOutTime + this.DurationMs + NotifyFadeInOutTime) - return Phase.Expired; - else if (elapsed > NotifyFadeInOutTime + this.DurationMs) - return Phase.FadeOut; - else if (elapsed > NotifyFadeInOutTime) - return Phase.Wait; - else - return Phase.FadeIn; - } - - /// - /// Gets the opacity of the notification. - /// - /// The opacity, in a range from 0 to 1. - internal float GetFadePercent() - { - var phase = this.GetPhase(); - var elapsed = this.ElapsedTime.TotalMilliseconds; - - if (phase == Phase.FadeIn) - { - return (float)elapsed / NotifyFadeInOutTime * NotifyOpacity; - } - else if (phase == Phase.FadeOut) - { - return (1.0f - (((float)elapsed - NotifyFadeInOutTime - this.DurationMs) / - NotifyFadeInOutTime)) * NotifyOpacity; - } - - return 1.0f * NotifyOpacity; - } - } -} diff --git a/Dalamud/Interface/Internal/Notifications/NotificationType.cs b/Dalamud/Interface/Internal/Notifications/NotificationType.cs index 1885ec809..5fffbe9af 100644 --- a/Dalamud/Interface/Internal/Notifications/NotificationType.cs +++ b/Dalamud/Interface/Internal/Notifications/NotificationType.cs @@ -1,32 +1,23 @@ -namespace Dalamud.Interface.Internal.Notifications; +using Dalamud.Utility; -/// -/// Possible notification types. -/// +namespace Dalamud.Interface.Internal.Notifications; + +/// Possible notification types. +[Api10ToDo(Api10ToDoAttribute.MoveNamespace, nameof(ImGuiNotification.Internal))] public enum NotificationType { - /// - /// No special type. - /// + /// No special type. None, - /// - /// Type indicating success. - /// + /// Type indicating success. Success, - /// - /// Type indicating a warning. - /// + /// Type indicating a warning. Warning, - /// - /// Type indicating an error. - /// + /// Type indicating an error. Error, - /// - /// Type indicating generic information. - /// + /// Type indicating generic information. Info, } diff --git a/Dalamud/Interface/Internal/TextureManager.cs b/Dalamud/Interface/Internal/TextureManager.cs index 9f90ea1ad..74ce91e5e 100644 --- a/Dalamud/Interface/Internal/TextureManager.cs +++ b/Dalamud/Interface/Internal/TextureManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Interface.Internal; [ResolveVia] [ResolveVia] #pragma warning restore SA1015 -internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITextureSubstitutionProvider +internal class TextureManager : IInternalDisposableService, ITextureProvider, ITextureSubstitutionProvider { private const string IconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}.tex"; private const string HighResolutionIconFileFormat = "ui/icon/{0:D3}000/{1}{2:D6}_hr1.tex"; @@ -268,7 +268,7 @@ internal class TextureManager : IDisposable, IServiceType, ITextureProvider, ITe } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.fallbackTextureWrap?.Dispose(); this.framework.Update -= this.FrameworkOnUpdate; diff --git a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs index 0c9c90d0d..b0ca9c2aa 100644 --- a/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs +++ b/Dalamud/Interface/Internal/Windows/ConsoleWindow.cs @@ -12,6 +12,8 @@ using Dalamud.Game; using Dalamud.Game.Command; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; @@ -76,6 +78,8 @@ internal class ConsoleWindow : Window, IDisposable private int historyPos; private int copyStart = -1; + private IActiveNotification? prevCopyNotification; + /// Initializes a new instance of the class. /// An instance of . public ConsoleWindow(DalamudConfiguration configuration) @@ -436,10 +440,14 @@ internal class ConsoleWindow : Window, IDisposable return; ImGui.SetClipboardText(sb.ToString()); - Service.Get().AddNotification( - $"{n:n0} line(s) copied.", - this.WindowName, - NotificationType.Success); + this.prevCopyNotification?.DismissNow(); + this.prevCopyNotification = Service.Get().AddNotification( + new() + { + Title = this.WindowName, + Content = $"{n:n0} line(s) copied.", + Type = NotificationType.Success, + }); } private void DrawOptionsToolbar() diff --git a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs index c19f56654..5cede00cf 100644 --- a/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/GameInventoryTestWidget.cs @@ -74,7 +74,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.standardEnabled = false; if (!this.rawEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -105,7 +105,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget this.rawEnabled = false; if (!this.standardEnabled) { - this.scoped.Dispose(); + ((IInternalDisposableService)this.scoped).DisposeService(); this.scoped = null; } } @@ -135,7 +135,7 @@ internal class GameInventoryTestWidget : IDataWindowWidget { if (ImGui.Button("Disable##all-disable")) { - this.scoped?.Dispose(); + ((IInternalDisposableService)this.scoped)?.DisposeService(); this.scoped = null; this.standardEnabled = this.rawEnabled = false; } diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs index 5b2855298..26af2a8b2 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/AddonLifecycleWidget.cs @@ -90,7 +90,7 @@ public class AddonLifecycleWidget : IDataWindowWidget ImGui.Text(listener.AddonName is "" ? "GLOBAL" : listener.AddonName); ImGui.TableNextColumn(); - ImGui.Text($"{listener.FunctionDelegate.Target}::{listener.FunctionDelegate.Method.Name}"); + ImGui.Text($"{listener.FunctionDelegate.Method.DeclaringType.FullName}::{listener.FunctionDelegate.Method.Name}"); } ImGui.EndTable(); diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs index 92f340a7b..346255dfe 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/DataShareWidget.cs @@ -5,6 +5,7 @@ using System.Numerics; using System.Reflection; using System.Text; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs index 2c7ceb95b..086b0c1ad 100644 --- a/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs +++ b/Dalamud/Interface/Internal/Windows/Data/Widgets/ImGuiWidget.cs @@ -1,5 +1,14 @@ -using Dalamud.Interface.Internal.Notifications; +using System.Linq; +using System.Threading.Tasks; + +using Dalamud.Game.Text; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; +using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; +using Dalamud.Storage.Assets; +using Dalamud.Utility; + using ImGuiNET; namespace Dalamud.Interface.Internal.Windows.Data.Widgets; @@ -9,11 +18,13 @@ namespace Dalamud.Interface.Internal.Windows.Data.Widgets; /// internal class ImGuiWidget : IDataWindowWidget { + private NotificationTemplate notificationTemplate; + /// public string[]? CommandShortcuts { get; init; } = { "imgui" }; - + /// - public string DisplayName { get; init; } = "ImGui"; + public string DisplayName { get; init; } = "ImGui"; /// public bool Ready { get; set; } @@ -22,6 +33,7 @@ internal class ImGuiWidget : IDataWindowWidget public void Load() { this.Ready = true; + this.notificationTemplate.Reset(); } /// @@ -38,38 +50,374 @@ internal class ImGuiWidget : IDataWindowWidget ImGui.Separator(); - ImGui.TextUnformatted($"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); + ImGui.TextUnformatted( + $"WindowSystem.TimeSinceLastAnyFocus: {WindowSystem.TimeSinceLastAnyFocus.TotalMilliseconds:0}ms"); ImGui.Separator(); - if (ImGui.Button("Add random notification")) + ImGui.Checkbox("##manualContent", ref this.notificationTemplate.ManualContent); + ImGui.SameLine(); + ImGui.InputText("Content##content", ref this.notificationTemplate.Content, 255); + + ImGui.Checkbox("##manualTitle", ref this.notificationTemplate.ManualTitle); + ImGui.SameLine(); + ImGui.InputText("Title##title", ref this.notificationTemplate.Title, 255); + + ImGui.Checkbox("##manualMinimizedText", ref this.notificationTemplate.ManualMinimizedText); + ImGui.SameLine(); + ImGui.InputText("MinimizedText##minimizedText", ref this.notificationTemplate.MinimizedText, 255); + + ImGui.Checkbox("##manualType", ref this.notificationTemplate.ManualType); + ImGui.SameLine(); + ImGui.Combo( + "Type##type", + ref this.notificationTemplate.TypeInt, + NotificationTemplate.TypeTitles, + NotificationTemplate.TypeTitles.Length); + + ImGui.Combo( + "Icon##iconCombo", + ref this.notificationTemplate.IconInt, + NotificationTemplate.IconTitles, + NotificationTemplate.IconTitles.Length); + switch (this.notificationTemplate.IconInt) { - var rand = new Random(); + case 1 or 2: + ImGui.InputText( + "Icon Text##iconText", + ref this.notificationTemplate.IconText, + 255); + break; + case 5 or 6: + ImGui.Combo( + "Asset##iconAssetCombo", + ref this.notificationTemplate.IconAssetInt, + NotificationTemplate.AssetSources, + NotificationTemplate.AssetSources.Length); + break; + case 3 or 7: + ImGui.InputText( + "Game Path##iconText", + ref this.notificationTemplate.IconText, + 255); + break; + case 4 or 8: + ImGui.InputText( + "File Path##iconText", + ref this.notificationTemplate.IconText, + 255); + break; + } - var title = rand.Next(0, 5) switch + ImGui.Combo( + "Initial Duration", + ref this.notificationTemplate.InitialDurationInt, + NotificationTemplate.InitialDurationTitles, + NotificationTemplate.InitialDurationTitles.Length); + + ImGui.Combo( + "Extension Duration", + ref this.notificationTemplate.HoverExtendDurationInt, + NotificationTemplate.HoverExtendDurationTitles, + NotificationTemplate.HoverExtendDurationTitles.Length); + + ImGui.Combo( + "Progress", + ref this.notificationTemplate.ProgressMode, + NotificationTemplate.ProgressModeTitles, + NotificationTemplate.ProgressModeTitles.Length); + + ImGui.Checkbox("Respect UI Hidden", ref this.notificationTemplate.RespectUiHidden); + + ImGui.Checkbox("Minimized", ref this.notificationTemplate.Minimized); + + ImGui.Checkbox("Show Indeterminate If No Expiry", ref this.notificationTemplate.ShowIndeterminateIfNoExpiry); + + ImGui.Checkbox("User Dismissable", ref this.notificationTemplate.UserDismissable); + + ImGui.Checkbox( + "Action Bar (always on if not user dismissable for the example)", + ref this.notificationTemplate.ActionBar); + + if (ImGui.Button("Add notification")) + { + var text = + "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + + NewRandom(out var title, out var type, out var progress); + if (this.notificationTemplate.ManualTitle) + title = this.notificationTemplate.Title; + if (this.notificationTemplate.ManualContent) + text = this.notificationTemplate.Content; + if (this.notificationTemplate.ManualType) + type = (NotificationType)this.notificationTemplate.TypeInt; + + var n = notifications.AddNotification( + new() + { + Content = text, + Title = title, + MinimizedText = this.notificationTemplate.ManualMinimizedText + ? this.notificationTemplate.MinimizedText + : null, + Type = type, + ShowIndeterminateIfNoExpiry = this.notificationTemplate.ShowIndeterminateIfNoExpiry, + RespectUiHidden = this.notificationTemplate.RespectUiHidden, + Minimized = this.notificationTemplate.Minimized, + UserDismissable = this.notificationTemplate.UserDismissable, + InitialDuration = + this.notificationTemplate.InitialDurationInt == 0 + ? TimeSpan.MaxValue + : NotificationTemplate.Durations[this.notificationTemplate.InitialDurationInt], + ExtensionDurationSinceLastInterest = + this.notificationTemplate.HoverExtendDurationInt == 0 + ? TimeSpan.Zero + : NotificationTemplate.Durations[this.notificationTemplate.HoverExtendDurationInt], + Progress = this.notificationTemplate.ProgressMode switch + { + 0 => 1f, + 1 => progress, + 2 => 0f, + 3 => 0f, + 4 => -1f, + _ => 0.5f, + }, + Icon = this.notificationTemplate.IconInt switch + { + 1 => INotificationIcon.From( + (SeIconChar)(this.notificationTemplate.IconText.Length == 0 + ? 0 + : this.notificationTemplate.IconText[0])), + 2 => INotificationIcon.From( + (FontAwesomeIcon)(this.notificationTemplate.IconText.Length == 0 + ? 0 + : this.notificationTemplate.IconText[0])), + 3 => INotificationIcon.FromGame(this.notificationTemplate.IconText), + 4 => INotificationIcon.FromFile(this.notificationTemplate.IconText), + _ => null, + }, + }); + + var dam = Service.Get(); + var tm = Service.Get(); + switch (this.notificationTemplate.IconInt) { - 0 => "This is a toast", - 1 => "Truly, a toast", - 2 => "I am testing this toast", - 3 => "I hope this looks right", - 4 => "Good stuff", - 5 => "Nice", - _ => null, - }; + case 5: + n.SetIconTexture( + dam.GetDalamudTextureWrap( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); + break; + case 6: + n.SetIconTexture( + dam.GetDalamudTextureWrapAsync( + Enum.Parse( + NotificationTemplate.AssetSources[this.notificationTemplate.IconAssetInt]))); + break; + case 7: + n.SetIconTexture(tm.GetTextureFromGame(this.notificationTemplate.IconText)); + break; + case 8: + n.SetIconTexture(tm.GetTextureFromFile(new(this.notificationTemplate.IconText))); + break; + } - var type = rand.Next(0, 4) switch + switch (this.notificationTemplate.ProgressMode) { - 0 => NotificationType.Error, - 1 => NotificationType.Warning, - 2 => NotificationType.Info, - 3 => NotificationType.Success, - 4 => NotificationType.None, - _ => NotificationType.None, - }; + case 2: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } + }); + break; + case 3: + Task.Run( + async () => + { + for (var i = 0; i <= 10 && !n.DismissReason.HasValue; i++) + { + await Task.Delay(500); + n.Progress = i / 10f; + } - const string text = "Bla bla bla bla bla bla bla bla bla bla bla.\nBla bla bla bla bla bla bla bla bla bla bla bla bla bla."; + n.ExtendBy(NotificationConstants.DefaultDuration); + n.InitialDuration = NotificationConstants.DefaultDuration; + }); + break; + } - notifications.AddNotification(text, title, type); + if (this.notificationTemplate.ActionBar || !this.notificationTemplate.UserDismissable) + { + var nclick = 0; + var testString = "input"; + + n.Click += _ => nclick++; + n.DrawActions += an => + { + ImGui.AlignTextToFramePadding(); + ImGui.TextUnformatted($"{nclick}"); + + ImGui.SameLine(); + if (ImGui.Button("Update")) + { + NewRandom(out title, out type, out progress); + an.Notification.Title = title; + an.Notification.Type = type; + an.Notification.Progress = progress; + } + + ImGui.SameLine(); + if (ImGui.Button("Dismiss")) + an.Notification.DismissNow(); + + ImGui.SameLine(); + ImGui.SetNextItemWidth(an.MaxCoord.X - ImGui.GetCursorPosX()); + ImGui.InputText("##input", ref testString, 255); + }; + } + } + } + + private static void NewRandom(out string? title, out NotificationType type, out float progress) + { + var rand = new Random(); + + title = rand.Next(0, 7) switch + { + 0 => "This is a toast", + 1 => "Truly, a toast", + 2 => "I am testing this toast", + 3 => "I hope this looks right", + 4 => "Good stuff", + 5 => "Nice", + _ => null, + }; + + type = rand.Next(0, 5) switch + { + 0 => NotificationType.Error, + 1 => NotificationType.Warning, + 2 => NotificationType.Info, + 3 => NotificationType.Success, + 4 => NotificationType.None, + _ => NotificationType.None, + }; + + if (rand.Next() % 2 == 0) + progress = -1; + else + progress = rand.NextSingle(); + } + + private struct NotificationTemplate + { + public static readonly string[] IconTitles = + { + "None (use Type)", + "SeIconChar", + "FontAwesomeIcon", + "GamePath", + "FilePath", + "TextureWrap from DalamudAssets", + "TextureWrap from DalamudAssets(Async)", + "TextureWrap from GamePath", + "TextureWrap from FilePath", + }; + + public static readonly string[] AssetSources = + Enum.GetValues() + .Where(x => x.GetAttribute()?.Purpose is DalamudAssetPurpose.TextureFromPng) + .Select(Enum.GetName) + .ToArray(); + + public static readonly string[] ProgressModeTitles = + { + "Default", + "Random", + "Increasing", + "Increasing & Auto Dismiss", + "Indeterminate", + }; + + public static readonly string[] TypeTitles = + { + nameof(NotificationType.None), + nameof(NotificationType.Success), + nameof(NotificationType.Warning), + nameof(NotificationType.Error), + nameof(NotificationType.Info), + }; + + public static readonly string[] InitialDurationTitles = + { + "Infinite", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + + public static readonly string[] HoverExtendDurationTitles = + { + "Disable", + "1 seconds", + "3 seconds (default)", + "10 seconds", + }; + + public static readonly TimeSpan[] Durations = + { + TimeSpan.Zero, + TimeSpan.FromSeconds(1), + NotificationConstants.DefaultDuration, + TimeSpan.FromSeconds(10), + }; + + public bool ManualContent; + public string Content; + public bool ManualTitle; + public string Title; + public bool ManualMinimizedText; + public string MinimizedText; + public int IconInt; + public string IconText; + public int IconAssetInt; + public bool ManualType; + public int TypeInt; + public int InitialDurationInt; + public int HoverExtendDurationInt; + public bool ShowIndeterminateIfNoExpiry; + public bool RespectUiHidden; + public bool Minimized; + public bool UserDismissable; + public bool ActionBar; + public int ProgressMode; + + public void Reset() + { + this.ManualContent = false; + this.Content = string.Empty; + this.ManualTitle = false; + this.Title = string.Empty; + this.ManualMinimizedText = false; + this.MinimizedText = string.Empty; + this.IconInt = 0; + this.IconText = "ui/icon/000000/000004_hr1.tex"; + this.IconAssetInt = 0; + this.ManualType = false; + this.TypeInt = (int)NotificationType.None; + this.InitialDurationInt = 2; + this.HoverExtendDurationInt = 2; + this.ShowIndeterminateIfNoExpiry = true; + this.Minimized = true; + this.UserDismissable = true; + this.ActionBar = true; + this.ProgressMode = 0; + this.RespectUiHidden = true; } } } diff --git a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs index 29adbb3e5..97744b1a7 100644 --- a/Dalamud/Interface/Internal/Windows/PluginImageCache.cs +++ b/Dalamud/Interface/Internal/Windows/PluginImageCache.cs @@ -21,7 +21,7 @@ namespace Dalamud.Interface.Internal.Windows; /// A cache for plugin icons and images. /// [ServiceManager.EarlyLoadedService] -internal class PluginImageCache : IDisposable, IServiceType +internal class PluginImageCache : IInternalDisposableService { /// /// Maximum plugin image width. @@ -136,7 +136,7 @@ internal class PluginImageCache : IDisposable, IServiceType this.dalamudAssetManager.GetDalamudTextureWrap(DalamudAsset.LogoSmall, this.EmptyTexture); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancelToken.Cancel(); this.downloadQueue.CompleteAdding(); diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs index 95c227662..210290f17 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/PluginInstallerWindow.cs @@ -15,6 +15,7 @@ using Dalamud.Game.Command; using Dalamud.Interface.Animation.EasingFunctions; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs index eafea9d16..857002771 100644 --- a/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs +++ b/Dalamud/Interface/Internal/Windows/PluginInstaller/ProfileManagerWidget.cs @@ -7,6 +7,7 @@ using CheapLoc; using Dalamud.Configuration.Internal; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Utility; using Dalamud.Interface.Utility.Raii; diff --git a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs index a1d93bb8c..bfa30cafd 100644 --- a/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs +++ b/Dalamud/Interface/Internal/Windows/PluginStatWindow.cs @@ -7,6 +7,7 @@ using System.Reflection; using Dalamud.Game; using Dalamud.Hooking.Internal; using Dalamud.Interface.Components; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.Windowing; using Dalamud.Plugin.Internal; diff --git a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs index a79ab099d..2feac8849 100644 --- a/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs +++ b/Dalamud/Interface/ManagedFontAtlas/IFontAtlas.cs @@ -85,6 +85,10 @@ public interface IFontAtlas : IDisposable /// Creates a new from game's built-in fonts. /// Font to use. /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. /// This function does not throw. will be populated instead, if /// the build procedure has failed. can be used regardless of the state of the font /// handle. @@ -93,6 +97,13 @@ public interface IFontAtlas : IDisposable /// Creates a new IFontHandle using your own callbacks. /// Callback for . /// Handle to a font that may or may not be ready yet. + /// When called during , + /// , , and alike. Move the font handle + /// creating code outside those handlers, and only initialize them once. Call + /// on a previous font handle if you're replacing one. + /// Consider calling to + /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language + /// users. /// /// Consider calling to /// support glyphs that are not supplied by the game by default; this mostly affects Chinese and Korean language diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs index 883fcbbfc..3c175ae3c 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.Implementation.cs @@ -35,6 +35,9 @@ internal sealed partial class FontAtlasFactory /// public const string EllipsisCodepoints = "\u2026\u0085"; + /// Marker for tasks on whether it's being called inside a font build cycle. + public static readonly AsyncLocal IsBuildInProgressForTask = new(); + /// /// If set, disables concurrent font build operation. /// @@ -204,12 +207,12 @@ internal sealed partial class FontAtlasFactory { while (this.IsBuildInProgress) await Task.Delay(100); - this.Garbage.Dispose(); + this.Clear(); }); } else { - this.Garbage.Dispose(); + this.Clear(); } return newRefCount; @@ -227,6 +230,20 @@ internal sealed partial class FontAtlasFactory var axisSubstance = this.Substances.OfType().Single(); return new(factory, this, axisSubstance, isAsync) { BuildStep = FontAtlasBuildStep.PreBuild }; } + + public void Clear() + { + try + { + this.Garbage.Dispose(); + } + catch (Exception e) + { + Log.Error( + e, + $"Disposing {nameof(FontAtlasBuiltData)} of {this.Owner?.Name ?? "???"}."); + } + } } private class DalamudFontAtlas : IFontAtlas, DisposeSafety.IDisposeCallback @@ -413,11 +430,28 @@ internal sealed partial class FontAtlasFactory } /// - public IFontHandle NewGameFontHandle(GameFontStyle style) => this.gameFontHandleManager.NewFontHandle(style); + public IFontHandle NewGameFontHandle(GameFontStyle style) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewGameFontHandle)} may not be called during {nameof(this.BuildStepChange)}, the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.gameFontHandleManager.NewFontHandle(style); + } /// - public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) => - this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + public IFontHandle NewDelegateFontHandle(FontAtlasBuildStepDelegate buildStepDelegate) + { + if (IsBuildInProgressForTask.Value) + { + throw new InvalidOperationException( + $"{nameof(this.NewDelegateFontHandle)} may not be called during {nameof(this.BuildStepChange)} or the callback of {nameof(this.NewDelegateFontHandle)}, {nameof(UiBuilder.BuildFonts)} or {nameof(UiBuilder.AfterBuildFonts)}."); + } + + return this.delegateFontHandleManager.NewFontHandle(buildStepDelegate); + } /// public void BuildFontsOnNextFrame() @@ -547,13 +581,13 @@ internal sealed partial class FontAtlasFactory { if (this.buildIndex != rebuildIndex) { - data.ExplicitDisposeIgnoreExceptions(); + data.Release(); return; } var prevBuiltData = this.builtData; this.builtData = data; - prevBuiltData.ExplicitDisposeIgnoreExceptions(); + prevBuiltData?.Release(); this.buildTask = EmptyTask; fontsAndLocks.EnsureCapacity(data.Substances.Sum(x => x.RelevantHandles.Count)); @@ -616,6 +650,8 @@ internal sealed partial class FontAtlasFactory FontAtlasBuiltData? res = null; nint atlasPtr = 0; BuildToolkit? toolkit = null; + + IsBuildInProgressForTask.Value = true; try { res = new(this, scale); @@ -740,6 +776,7 @@ internal sealed partial class FontAtlasFactory // ReSharper disable once ConstantConditionalAccessQualifier toolkit?.Dispose(); this.buildQueued = false; + IsBuildInProgressForTask.Value = false; } unsafe bool ValidateMergeFontReferences(ImFontPtr replacementDstFont) diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs index 3e0fd1394..7fa41487a 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontAtlasFactory.cs @@ -31,7 +31,7 @@ namespace Dalamud.Interface.ManagedFontAtlas.Internals; /// [ServiceManager.BlockingEarlyLoadedService] internal sealed partial class FontAtlasFactory - : IServiceType, GamePrebakedFontHandle.IGameFontTextureProvider, IDisposable + : IInternalDisposableService, GamePrebakedFontHandle.IGameFontTextureProvider { private readonly DisposeSafety.ScopedFinalizer scopedFinalizer = new(); private readonly CancellationTokenSource cancellationTokenSource = new(); @@ -161,7 +161,7 @@ internal sealed partial class FontAtlasFactory this.dalamudAssetManager.IsStreamImmediatelyAvailable(DalamudAsset.LodestoneGameSymbol); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cancellationTokenSource.Cancel(); this.scopedFinalizer.Dispose(); diff --git a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs index 89d968158..0e26145f0 100644 --- a/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs +++ b/Dalamud/Interface/ManagedFontAtlas/Internals/FontHandle.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Threading; using System.Threading.Tasks; using Dalamud.Interface.Internal; @@ -24,7 +25,6 @@ internal abstract class FontHandle : IFontHandle private static readonly ConditionalWeakTable NonMainThreadFontAccessWarning = new(); private static long nextNonMainThreadFontAccessWarningCheck; - private readonly InterfaceManager interfaceManager; private readonly List pushedFonts = new(8); private IFontHandleManager? manager; @@ -36,7 +36,6 @@ internal abstract class FontHandle : IFontHandle /// An instance of . protected FontHandle(IFontHandleManager manager) { - this.interfaceManager = Service.Get(); this.manager = manager; } @@ -58,7 +57,11 @@ internal abstract class FontHandle : IFontHandle /// Gets the associated . /// /// When the object has already been disposed. - protected IFontHandleManager Manager => this.manager ?? throw new ObjectDisposedException(this.GetType().Name); + protected IFontHandleManager Manager => + this.manager + ?? throw new ObjectDisposedException( + this.GetType().Name, + "Did you write `using (fontHandle)` instead of `using (fontHandle.Push())`?"); /// public void Dispose() @@ -122,7 +125,7 @@ internal abstract class FontHandle : IFontHandle } } - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); return locked.ImFont; } @@ -201,7 +204,7 @@ internal abstract class FontHandle : IFontHandle ThreadSafety.AssertMainThread(); // Warn if the client is not properly managing the pushed font stack. - var cumulativePresentCalls = this.interfaceManager.CumulativePresentCalls; + var cumulativePresentCalls = Service.Get().CumulativePresentCalls; if (this.lastCumulativePresentCalls != cumulativePresentCalls) { this.lastCumulativePresentCalls = cumulativePresentCalls; @@ -218,7 +221,7 @@ internal abstract class FontHandle : IFontHandle if (this.TryLock(out _) is { } locked) { font = locked.ImFont; - this.interfaceManager.EnqueueDeferredDispose(locked); + Service.Get().EnqueueDeferredDispose(locked); } var rented = SimplePushedFont.Rent(this.pushedFonts, font); @@ -289,11 +292,15 @@ internal abstract class FontHandle : IFontHandle { if (disposing) { + if (Interlocked.Exchange(ref this.manager, null) is not { } managerToDisassociate) + return; + if (this.pushedFonts.Count > 0) Log.Warning($"{nameof(IFontHandle)}.{nameof(IDisposable.Dispose)}: fonts were still in a stack."); - this.Manager.FreeFontHandle(this); - this.manager = null; + + managerToDisassociate.FreeFontHandle(this); this.Disposed?.InvokeSafely(); + this.Disposed = null; this.ImFontChanged = null; } } diff --git a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs index 1f9a5bc76..6fbc0b4f3 100644 --- a/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs +++ b/Dalamud/Interface/TitleScreenMenu/TitleScreenMenu.cs @@ -193,7 +193,7 @@ internal class TitleScreenMenu : IServiceType, ITitleScreenMenu #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleScreenMenu +internal class TitleScreenMenuPluginScoped : IInternalDisposableService, ITitleScreenMenu { [ServiceManager.ServiceDependency] private readonly TitleScreenMenu titleScreenMenuService = Service.Get(); @@ -204,7 +204,7 @@ internal class TitleScreenMenuPluginScoped : IDisposable, IServiceType, ITitleSc public IReadOnlyList? Entries => this.titleScreenMenuService.Entries; /// - public void Dispose() + void IInternalDisposableService.DisposeService() { foreach (var entry in this.pluginEntries) { diff --git a/Dalamud/Interface/UiBuilder.cs b/Dalamud/Interface/UiBuilder.cs index d260868a0..2c2ca9725 100644 --- a/Dalamud/Interface/UiBuilder.cs +++ b/Dalamud/Interface/UiBuilder.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Threading.Tasks; @@ -9,12 +10,16 @@ using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Gui; using Dalamud.Interface.FontIdentifier; using Dalamud.Interface.GameFonts; +using Dalamud.Interface.ImGuiNotification; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal; using Dalamud.Interface.Internal.ManagedAsserts; using Dalamud.Interface.Internal.Notifications; using Dalamud.Interface.ManagedFontAtlas; using Dalamud.Interface.ManagedFontAtlas.Internals; +using Dalamud.Plugin; using Dalamud.Plugin.Internal.Types; +using Dalamud.Plugin.Services; using Dalamud.Utility; using ImGuiNET; using ImGuiScene; @@ -29,11 +34,13 @@ namespace Dalamud.Interface; /// public sealed class UiBuilder : IDisposable { + private readonly LocalPlugin localPlugin; private readonly Stopwatch stopwatch; private readonly HitchDetector hitchDetector; private readonly string namespaceName; private readonly InterfaceManager interfaceManager = Service.Get(); private readonly Framework framework = Service.Get(); + private readonly ConcurrentDictionary notifications = new(); [ServiceManager.ServiceDependency] private readonly DalamudConfiguration configuration = Service.Get(); @@ -52,8 +59,10 @@ public sealed class UiBuilder : IDisposable /// You do not have to call this manually. /// /// The plugin namespace. - internal UiBuilder(string namespaceName) + /// The relevant local plugin. + internal UiBuilder(string namespaceName, LocalPlugin localPlugin) { + this.localPlugin = localPlugin; try { this.stopwatch = new Stopwatch(); @@ -507,9 +516,16 @@ public sealed class UiBuilder : IDisposable /// Handle to the game font which may or may not be available for use yet. [Obsolete($"Use {nameof(this.FontAtlas)}.{nameof(IFontAtlas.NewGameFontHandle)} instead.", false)] [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] - public GameFontHandle GetGameFontHandle(GameFontStyle style) => new( - (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), - Service.Get()); + public GameFontHandle GetGameFontHandle(GameFontStyle style) + { + var prevValue = FontAtlasFactory.IsBuildInProgressForTask.Value; + FontAtlasFactory.IsBuildInProgressForTask.Value = false; + var v = new GameFontHandle( + (GamePrebakedFontHandle)this.FontAtlas.NewGameFontHandle(style), + Service.Get()); + FontAtlasFactory.IsBuildInProgressForTask.Value = prevValue; + return v; + } /// /// Call this to queue a rebuild of the font atlas.
@@ -556,22 +572,50 @@ public sealed class UiBuilder : IDisposable /// The title of the notification. /// 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) + [Obsolete($"Use {nameof(INotificationManager)}.", false)] + [Api10ToDo(Api10ToDoAttribute.DeleteCompatBehavior)] + public async void AddNotification( + string content, + string? title = null, + NotificationType type = NotificationType.None, + uint msDelay = 3000) { - Service - .GetAsync() - .ContinueWith(task => + var nm = await Service.GetAsync(); + var an = nm.AddNotification( + new() { - if (task.IsCompletedSuccessfully) - task.Result.AddNotification(content, title, type, msDelay); - }); + Content = content, + Title = title, + Type = type, + InitialDuration = TimeSpan.FromMilliseconds(msDelay), + }, + this.localPlugin); + _ = this.notifications.TryAdd(an, 0); + an.Dismiss += a => this.notifications.TryRemove(a.Notification, out _); } /// /// Unregister the UiBuilder. Do not call this in plugin code. /// - void IDisposable.Dispose() => this.scopedFinalizer.Dispose(); + void IDisposable.Dispose() + { + this.scopedFinalizer.Dispose(); + + // Taken from NotificationManagerPluginScoped. + // TODO: remove on API 10. + while (!this.notifications.IsEmpty) + { + foreach (var n in this.notifications.Keys) + { + this.notifications.TryRemove(n, out _); + ((ActiveNotification)n).RemoveNonDalamudInvocations(); + } + } + } + + /// Clean up resources allocated by this instance of . + /// Dalamud internal use only. + internal void DisposeInternal() => this.scopedFinalizer.Dispose(); /// /// Open the registered configuration UI, if it exists. diff --git a/Dalamud/Interface/Utility/ImGuiHelpers.cs b/Dalamud/Interface/Utility/ImGuiHelpers.cs index f02effe1d..639b0315d 100644 --- a/Dalamud/Interface/Utility/ImGuiHelpers.cs +++ b/Dalamud/Interface/Utility/ImGuiHelpers.cs @@ -493,12 +493,13 @@ public static class ImGuiHelpers /// The range array that can be used for . public static ushort[] CreateImGuiRangesFrom(IEnumerable ranges) => ranges - .Where(x => x.FirstCodePoint <= ushort.MaxValue) + .Select(x => (First: Math.Max(x.FirstCodePoint, 1), Last: x.FirstCodePoint + x.Length)) + .Where(x => x.First <= ushort.MaxValue && x.First <= x.Last) .SelectMany( x => new[] { - (ushort)Math.Min(x.FirstCodePoint, ushort.MaxValue), - (ushort)Math.Min(x.FirstCodePoint + x.Length, ushort.MaxValue), + (ushort)Math.Min(x.First, ushort.MaxValue), + (ushort)Math.Min(x.Last, ushort.MaxValue), }) .Append((ushort)0) .ToArray(); diff --git a/Dalamud/IoC/Internal/ServiceScope.cs b/Dalamud/IoC/Internal/ServiceScope.cs index 01c18a8b2..9fcf1af3c 100644 --- a/Dalamud/IoC/Internal/ServiceScope.cs +++ b/Dalamud/IoC/Internal/ServiceScope.cs @@ -96,6 +96,17 @@ internal class ServiceScopeImpl : IServiceScope /// public void Dispose() { - foreach (var createdObject in this.scopeCreatedObjects.OfType()) createdObject.Dispose(); + foreach (var createdObject in this.scopeCreatedObjects) + { + switch (createdObject) + { + case IInternalDisposableService d: + d.DisposeService(); + break; + case IDisposable d: + d.Dispose(); + break; + } + } } } diff --git a/Dalamud/Localization.cs b/Dalamud/Localization.cs index b180f113a..a9b0cf93d 100644 --- a/Dalamud/Localization.cs +++ b/Dalamud/Localization.cs @@ -36,6 +36,7 @@ public class Localization : IServiceType /// Use embedded loc resource files. public Localization(string locResourceDirectory, string locResourcePrefix = "", bool useEmbedded = false) { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.locResourceDirectory = locResourceDirectory; this.locResourcePrefix = locResourcePrefix; this.useEmbedded = useEmbedded; @@ -61,7 +62,24 @@ public class Localization : IServiceType /// /// Event that occurs when the language is changed. /// - public event LocalizationChangedDelegate LocalizationChanged; + public event LocalizationChangedDelegate? LocalizationChanged; + + /// + /// Gets an instance of that corresponds to the language configured from Dalamud Settings. + /// + public CultureInfo DalamudLanguageCultureInfo { get; private set; } + + /// + /// Gets an instance of that corresponds to . + /// + /// The language code which should be in . + /// The corresponding instance of . + public static CultureInfo GetCultureInfoFromLangCode(string langCode) => + CultureInfo.GetCultureInfo(langCode switch + { + "tw" => "zh-tw", + _ => langCode, + }); /// /// Search the set-up localization data for the provided assembly for the given string key and return it. @@ -108,6 +126,7 @@ public class Localization : IServiceType /// public void SetupWithFallbacks() { + this.DalamudLanguageCultureInfo = CultureInfo.InvariantCulture; this.LocalizationChanged?.Invoke(FallbackLangCode); Loc.SetupWithFallbacks(this.assembly); } @@ -124,6 +143,7 @@ public class Localization : IServiceType return; } + this.DalamudLanguageCultureInfo = GetCultureInfoFromLangCode(langCode); this.LocalizationChanged?.Invoke(langCode); try diff --git a/Dalamud/Logging/Internal/TaskTracker.cs b/Dalamud/Logging/Internal/TaskTracker.cs index da4007570..ae3dae5e9 100644 --- a/Dalamud/Logging/Internal/TaskTracker.cs +++ b/Dalamud/Logging/Internal/TaskTracker.cs @@ -13,7 +13,7 @@ namespace Dalamud.Logging.Internal; /// Class responsible for tracking asynchronous tasks. /// [ServiceManager.EarlyLoadedService] -internal class TaskTracker : IDisposable, IServiceType +internal class TaskTracker : IInternalDisposableService { private static readonly ModuleLog Log = new("TT"); private static readonly List TrackedTasksInternal = new(); @@ -120,7 +120,7 @@ internal class TaskTracker : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { // NET8 CHORE // this.scheduleAndStartHook?.Dispose(); diff --git a/Dalamud/Logging/ScopedPluginLogService.cs b/Dalamud/Logging/ScopedPluginLogService.cs index 924b4885d..0c044f2c2 100644 --- a/Dalamud/Logging/ScopedPluginLogService.cs +++ b/Dalamud/Logging/ScopedPluginLogService.cs @@ -17,7 +17,7 @@ namespace Dalamud.Logging; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable +internal class ScopedPluginLogService : IServiceType, IPluginLog { private readonly LocalPlugin localPlugin; @@ -53,12 +53,6 @@ internal class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable ///
public ILogger Logger { get; } - /// - public void Dispose() - { - GC.SuppressFinalize(this); - } - /// public void Fatal(string messageTemplate, params object[] values) => this.Write(LogEventLevel.Fatal, null, messageTemplate, values); diff --git a/Dalamud/Networking/Http/HappyHttpClient.cs b/Dalamud/Networking/Http/HappyHttpClient.cs index 4379a698f..23c6e3899 100644 --- a/Dalamud/Networking/Http/HappyHttpClient.cs +++ b/Dalamud/Networking/Http/HappyHttpClient.cs @@ -12,7 +12,7 @@ namespace Dalamud.Networking.Http; /// awareness. /// [ServiceManager.BlockingEarlyLoadedService] -internal class HappyHttpClient : IDisposable, IServiceType +internal class HappyHttpClient : IInternalDisposableService { /// /// Initializes a new instance of the class. @@ -58,7 +58,7 @@ internal class HappyHttpClient : IDisposable, IServiceType public HappyEyeballsCallback SharedHappyEyeballsCallback { get; } /// - void IDisposable.Dispose() + void IInternalDisposableService.DisposeService() { this.SharedHttpClient.Dispose(); this.SharedHappyEyeballsCallback.Dispose(); diff --git a/Dalamud/Plugin/DalamudPluginInterface.cs b/Dalamud/Plugin/DalamudPluginInterface.cs index 82f19aa49..135cf89ea 100644 --- a/Dalamud/Plugin/DalamudPluginInterface.cs +++ b/Dalamud/Plugin/DalamudPluginInterface.cs @@ -52,7 +52,7 @@ public sealed class DalamudPluginInterface : IDisposable var dataManager = Service.Get(); var localization = Service.Get(); - this.UiBuilder = new UiBuilder(plugin.Name); + this.UiBuilder = new UiBuilder(plugin.Name, plugin); this.configs = Service.Get().PluginConfigs; this.Reason = reason; @@ -452,26 +452,28 @@ public sealed class DalamudPluginInterface : IDisposable #endregion - /// - /// Unregister your plugin and dispose all references. - /// + /// void IDisposable.Dispose() { - this.UiBuilder.ExplicitDispose(); - Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); - Service.Get().LocalizationChanged -= this.OnLocalizationChanged; - Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; } - /// - /// Obsolete implicit dispose implementation. Should not be used. - /// - [Obsolete("Do not dispose \"DalamudPluginInterface\".", true)] + /// This function will do nothing. Dalamud will dispose this object on plugin unload. + [Obsolete("This function will do nothing. Dalamud will dispose this object on plugin unload.", true)] public void Dispose() { // ignored } + /// Unregister the plugin and dispose all references. + /// Dalamud internal use only. + internal void DisposeInternal() + { + Service.Get().RemoveChatLinkHandler(this.plugin.InternalName); + Service.Get().LocalizationChanged -= this.OnLocalizationChanged; + Service.Get().DalamudConfigurationSaved -= this.OnDalamudConfigurationSaved; + this.UiBuilder.DisposeInternal(); + } + /// /// Dispatch the active plugins changed event. /// diff --git a/Dalamud/Plugin/Internal/PluginManager.cs b/Dalamud/Plugin/Internal/PluginManager.cs index fce39d83c..4ef7e8320 100644 --- a/Dalamud/Plugin/Internal/PluginManager.cs +++ b/Dalamud/Plugin/Internal/PluginManager.cs @@ -55,7 +55,7 @@ namespace Dalamud.Plugin.Internal; [InherentDependency] #pragma warning restore SA1015 -internal partial class PluginManager : IDisposable, IServiceType +internal partial class PluginManager : IInternalDisposableService { /// /// Default time to wait between plugin unload and plugin assembly unload. @@ -371,7 +371,7 @@ internal partial class PluginManager : IDisposable, IServiceType } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { var disposablePlugins = this.installedPluginsList.Where(plugin => plugin.State is PluginState.Loaded or PluginState.LoadError).ToArray(); @@ -411,7 +411,16 @@ internal partial class PluginManager : IDisposable, IServiceType // 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 disposablePlugins) - plugin.ExplicitDisposeIgnoreExceptions($"Error disposing {plugin.Name}", Log); + { + try + { + plugin.Dispose(); + } + catch (Exception e) + { + Log.Error(e, $"Error disposing {plugin.Name}"); + } + } } // NET8 CHORE diff --git a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs index 7001e4d7b..eebb87aaa 100644 --- a/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs +++ b/Dalamud/Plugin/Internal/Profiles/ProfileCommandHandler.cs @@ -16,7 +16,7 @@ namespace Dalamud.Plugin.Internal.Profiles; /// Service responsible for profile-related chat commands. /// [ServiceManager.EarlyLoadedService] -internal class ProfileCommandHandler : IServiceType, IDisposable +internal class ProfileCommandHandler : IInternalDisposableService { private readonly CommandManager cmd; private readonly ProfileManager profileManager; @@ -69,7 +69,7 @@ internal class ProfileCommandHandler : IServiceType, IDisposable } /// - public void Dispose() + void IInternalDisposableService.DisposeService() { this.cmd.RemoveHandler("/xlenablecollection"); this.cmd.RemoveHandler("/xldisablecollection"); diff --git a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs index 580d5c161..1f9f503e0 100644 --- a/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalDevPlugin.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Dalamud.Configuration.Internal; +using Dalamud.Interface.ImGuiNotification.Internal; using Dalamud.Interface.Internal.Notifications; using Dalamud.Logging.Internal; using Dalamud.Plugin.Internal.Types.Manifest; diff --git a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs index e438c6f92..0c8777cfe 100644 --- a/Dalamud/Plugin/Internal/Types/LocalPlugin.cs +++ b/Dalamud/Plugin/Internal/Types/LocalPlugin.cs @@ -240,7 +240,7 @@ internal class LocalPlugin : IDisposable this.instance = null; } - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); @@ -427,7 +427,7 @@ internal class LocalPlugin : IDisposable if (this.instance == null) { this.State = PluginState.LoadError; - this.DalamudInterface.ExplicitDispose(); + this.DalamudInterface.DisposeInternal(); Log.Error( $"Error while loading {this.Name}, failed to bind and call the plugin constructor"); return; @@ -500,7 +500,7 @@ internal class LocalPlugin : IDisposable this.instance = null; - this.DalamudInterface?.ExplicitDispose(); + this.DalamudInterface?.DisposeInternal(); this.DalamudInterface = null; this.ServiceScope?.Dispose(); diff --git a/Dalamud/Plugin/Services/INotificationManager.cs b/Dalamud/Plugin/Services/INotificationManager.cs new file mode 100644 index 000000000..7d9ccd0b0 --- /dev/null +++ b/Dalamud/Plugin/Services/INotificationManager.cs @@ -0,0 +1,12 @@ +using Dalamud.Interface.ImGuiNotification; + +namespace Dalamud.Plugin.Services; + +/// Manager for notifications provided by Dalamud using ImGui. +public interface INotificationManager +{ + /// Adds a notification. + /// The new notification. + /// The added notification. + IActiveNotification AddNotification(Notification notification); +} diff --git a/Dalamud/ServiceManager.cs b/Dalamud/ServiceManager.cs index acd7c2b6f..845a65d6e 100644 --- a/Dalamud/ServiceManager.cs +++ b/Dalamud/ServiceManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -175,7 +176,8 @@ internal static class ServiceManager foreach (var serviceType in GetConcreteServiceTypes()) { var serviceKind = serviceType.GetServiceKind(); - Debug.Assert(serviceKind != ServiceKind.None, $"Service<{serviceType.FullName}> did not specify a kind"); + + CheckServiceTypeContracts(serviceType); // Let IoC know about the interfaces this service implements serviceContainer.RegisterInterfaces(serviceType); @@ -514,6 +516,44 @@ internal static class ServiceManager return ServiceKind.ProvidedService; } + /// Validate service type contracts, and throws exceptions accordingly. + /// An instance of that is supposed to be a service type. + /// Does nothing on non-debug builds. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void CheckServiceTypeContracts(Type serviceType) + { +#if DEBUG + try + { + if (!serviceType.IsAssignableTo(typeof(IServiceType))) + throw new InvalidOperationException($"Non-{nameof(IServiceType)} passed."); + if (serviceType.GetServiceKind() == ServiceKind.None) + throw new InvalidOperationException("Service type is not specified."); + + var isServiceDisposable = + serviceType.IsAssignableTo(typeof(IInternalDisposableService)); + var isAnyDisposable = + isServiceDisposable + || serviceType.IsAssignableTo(typeof(IDisposable)) + || serviceType.IsAssignableTo(typeof(IAsyncDisposable)); + if (isAnyDisposable && !isServiceDisposable) + { + throw new InvalidOperationException( + $"A service must be an {nameof(IInternalDisposableService)} without specifying " + + $"{nameof(IDisposable)} nor {nameof(IAsyncDisposable)} if it is purely meant to be a service, " + + $"or an {nameof(IPublicDisposableService)} if it also is allowed to be constructed not as a " + + $"service to be used elsewhere and has to offer {nameof(IDisposable)} or " + + $"{nameof(IAsyncDisposable)}. See {nameof(ReliableFileStorage)} for an example of " + + $"{nameof(IPublicDisposableService)}."); + } + } + catch (Exception e) + { + throw new InvalidOperationException($"{serviceType.Name}: {e.Message}"); + } +#endif + } + /// /// Indicates that this constructor will be called for early initialization. /// diff --git a/Dalamud/Service{T}.cs b/Dalamud/Service{T}.cs index 08f592826..ed03749d5 100644 --- a/Dalamud/Service{T}.cs +++ b/Dalamud/Service{T}.cs @@ -65,6 +65,12 @@ internal static class Service where T : IServiceType None, } + /// Does nothing. + /// Used to invoke the static ctor. + public static void Nop() + { + } + /// /// Sets the type in the service locator to the given object. /// @@ -72,6 +78,8 @@ internal static class Service where T : IServiceType public static void Provide(T obj) { ServiceManager.Log.Debug("Service<{0}>: Provided", typeof(T).Name); + if (obj is IPublicDisposableService pds) + pds.MarkDisposeOnlyFromService(); instanceTcs.SetResult(obj); } @@ -297,23 +305,26 @@ internal static class Service where T : IServiceType if (!instanceTcs.Task.IsCompletedSuccessfully) return; - var instance = instanceTcs.Task.Result; - if (instance is IDisposable disposable) + switch (instanceTcs.Task.Result) { - 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); + case IInternalDisposableService d: + ServiceManager.Log.Debug("Service<{0}>: Disposing", typeof(T).Name); + try + { + d.DisposeService(); + ServiceManager.Log.Debug("Service<{0}>: Disposed", typeof(T).Name); + } + catch (Exception e) + { + ServiceManager.Log.Warning(e, "Service<{0}>: Dispose failure", typeof(T).Name); + } + + break; + + default: + ServiceManager.CheckServiceTypeContracts(typeof(T)); + ServiceManager.Log.Debug("Service<{0}>: Unset", typeof(T).Name); + break; } instanceTcs = new TaskCompletionSource(); diff --git a/Dalamud/Storage/Assets/DalamudAssetManager.cs b/Dalamud/Storage/Assets/DalamudAssetManager.cs index 68be78352..4f53460fb 100644 --- a/Dalamud/Storage/Assets/DalamudAssetManager.cs +++ b/Dalamud/Storage/Assets/DalamudAssetManager.cs @@ -27,7 +27,7 @@ namespace Dalamud.Storage.Assets; #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 -internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudAssetManager +internal sealed class DalamudAssetManager : IInternalDisposableService, IDalamudAssetManager { private const int DownloadAttemptCount = 10; private const int RenameAttemptCount = 10; @@ -67,7 +67,13 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA .Where(x => x.GetAttribute()?.Required is true) .Select(this.CreateStreamAsync) .Select(x => x.ToContentDisposedTask())) - .ContinueWith(_ => loadTimings.Dispose()), + .ContinueWith( + r => + { + loadTimings.Dispose(); + return r; + }) + .Unwrap(), "Prevent Dalamud from loading more stuff, until we've ensured that all required assets are available."); Task.WhenAll( @@ -83,7 +89,7 @@ internal sealed class DalamudAssetManager : IServiceType, IDisposable, IDalamudA public IDalamudTextureWrap Empty4X4 => this.GetDalamudTextureWrap(DalamudAsset.Empty4X4); /// - public void Dispose() + void IInternalDisposableService.DisposeService() { lock (this.syncRoot) { diff --git a/Dalamud/Storage/ReliableFileStorage.cs b/Dalamud/Storage/ReliableFileStorage.cs index a013e95b5..eab93269e 100644 --- a/Dalamud/Storage/ReliableFileStorage.cs +++ b/Dalamud/Storage/ReliableFileStorage.cs @@ -22,17 +22,22 @@ namespace Dalamud.Storage; /// This is not an early-loaded service, as it is needed before they are initialized. /// [ServiceManager.ProvidedService] -public class ReliableFileStorage : IServiceType, IDisposable +[Api10ToDo("Make internal and IInternalDisposableService, and remove #pragma guard from the caller.")] +public class ReliableFileStorage : IPublicDisposableService { private static readonly ModuleLog Log = new("VFS"); private readonly object syncRoot = new(); + private SQLiteConnection? db; + private bool isService; /// /// Initializes a new instance of the class. /// /// Path to the VFS. + [Obsolete("Dalamud internal use only.", false)] + [Api10ToDo("Make internal, and remove #pragma guard from the caller.")] public ReliableFileStorage(string vfsDbPath) { var databasePath = Path.Combine(vfsDbPath, "dalamudVfs.db"); @@ -60,7 +65,7 @@ public class ReliableFileStorage : IServiceType, IDisposable } } } - + /// /// Check if a file exists. /// This will return true if the file does not exist on the filesystem, but in the transparent backup. @@ -288,9 +293,20 @@ public class ReliableFileStorage : IServiceType, IDisposable /// public void Dispose() { - this.db?.Dispose(); + if (!this.isService) + this.DisposeCore(); } + /// + void IInternalDisposableService.DisposeService() + { + if (this.isService) + this.DisposeCore(); + } + + /// + void IPublicDisposableService.MarkDisposeOnlyFromService() => this.isService = true; + /// /// Replace possible non-portable parts of a path with portable versions. /// @@ -312,6 +328,8 @@ public class ReliableFileStorage : IServiceType, IDisposable this.db.CreateTable(); } + private void DisposeCore() => this.db?.Dispose(); + private class DbFile { [PrimaryKey] diff --git a/Dalamud/Utility/Api10ToDoAttribute.cs b/Dalamud/Utility/Api10ToDoAttribute.cs index f397f8f0c..a13aaead5 100644 --- a/Dalamud/Utility/Api10ToDoAttribute.cs +++ b/Dalamud/Utility/Api10ToDoAttribute.cs @@ -11,9 +11,19 @@ internal sealed class Api10ToDoAttribute : Attribute /// public const string DeleteCompatBehavior = "Delete. This is for making API 9 plugins work."; + /// + /// Marks that this should be moved to an another namespace. + /// + public const string MoveNamespace = "Move to another namespace."; + /// /// Initializes a new instance of the class. /// /// The explanation. - public Api10ToDoAttribute(string what) => _ = what; + /// The explanation 2. + public Api10ToDoAttribute(string what, string what2 = "") + { + _ = what; + _ = what2; + } } diff --git a/Dalamud/Utility/DateTimeSpanExtensions.cs b/Dalamud/Utility/DateTimeSpanExtensions.cs new file mode 100644 index 000000000..8422a4a26 --- /dev/null +++ b/Dalamud/Utility/DateTimeSpanExtensions.cs @@ -0,0 +1,125 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; + +using CheapLoc; + +using Dalamud.Logging.Internal; + +namespace Dalamud.Utility; + +/// +/// Utility functions for and . +/// +public static class DateTimeSpanExtensions +{ + private static readonly ModuleLog Log = new(nameof(DateTimeSpanExtensions)); + + private static ParsedRelativeFormatStrings? relativeFormatStringLong; + + private static ParsedRelativeFormatStrings? relativeFormatStringShort; + + /// Formats an instance of as a localized absolute time. + /// When. + /// The formatted string. + /// The string will be formatted according to Square Enix Account region settings, if Dalamud default + /// language is English. + public static unsafe string LocAbsolute(this DateTime when) + { + var culture = Service.GetNullable()?.DalamudLanguageCultureInfo ?? CultureInfo.InvariantCulture; + if (!Equals(culture, CultureInfo.InvariantCulture)) + return when.ToString("G", culture); + + var framework = FFXIVClientStructs.FFXIV.Client.System.Framework.Framework.Instance(); + var region = 0; + if (framework is not null) + region = framework->Region; + return region switch + { + 1 => when.ToString("MM/dd/yyyy HH:mm:ss"), // na + 2 => when.ToString("dd-mm-yyyy HH:mm:ss"), // eu + _ => when.ToString("yyyy-MM-dd HH:mm:ss"), // jp(0), cn(3), kr(4), and other possible errorneous cases + }; + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastLong(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsLong", + "172800,{0:%d} days ago\n86400,yesterday\n7200,{0:%h} hours ago\n3600,an hour ago\n120,{0:%m} minutes ago\n60,a minute ago\n2,{0:%s} seconds ago\n1,a second ago\n-Infinity,just now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringLong?.FormatStringLoc != loc) + relativeFormatStringLong ??= new(loc); + + return relativeFormatStringLong.Format(DateTime.Now - when); + } + + /// Formats an instance of as a localized relative time. + /// When. + /// The formatted string. + public static string LocRelativePastShort(this DateTime when) + { + var loc = Loc.Localize( + "DateTimeSpanExtensions.RelativeFormatStringsShort", + "86400,{0:%d}d\n3600,{0:%h}h\n60,{0:%m}m\n1,{0:%s}s\n-Infinity,now"); + Debug.Assert(loc != null, "loc != null"); + + if (relativeFormatStringShort?.FormatStringLoc != loc) + relativeFormatStringShort = new(loc); + + return relativeFormatStringShort.Format(DateTime.Now - when); + } + + private sealed class ParsedRelativeFormatStrings + { + private readonly List<(float MinSeconds, string FormatString)> formatStrings = new(); + + public ParsedRelativeFormatStrings(string value) + { + this.FormatStringLoc = value; + foreach (var line in value.Split("\n")) + { + var sep = line.IndexOf(','); + if (sep < 0) + { + Log.Error("A line without comma has been found: {line}", line); + continue; + } + + if (!float.TryParse( + line.AsSpan(0, sep), + NumberStyles.Float, + CultureInfo.InvariantCulture, + out var seconds)) + { + Log.Error("Could not parse the duration: {line}", line); + continue; + } + + this.formatStrings.Add((seconds, line[(sep + 1)..])); + } + + this.formatStrings.Sort((a, b) => b.MinSeconds.CompareTo(a.MinSeconds)); + } + + public string FormatStringLoc { get; } + + /// Formats an instance of as a localized string. + /// The duration. + /// The formatted string. + public string Format(TimeSpan ts) + { + foreach (var (minSeconds, formatString) in this.formatStrings) + { + if (ts.TotalSeconds >= minSeconds) + return string.Format(formatString, ts); + } + + return this.formatStrings[^1].FormatString.Format(ts); + } + } +} diff --git a/Dalamud/Utility/DisposeSafety.cs b/Dalamud/Utility/DisposeSafety.cs index 8ac891e0a..64d31048f 100644 --- a/Dalamud/Utility/DisposeSafety.cs +++ b/Dalamud/Utility/DisposeSafety.cs @@ -70,7 +70,16 @@ public static class DisposeSafety r => { if (!r.IsCompletedSuccessfully) - return ignoreAllExceptions ? Task.CompletedTask : r; + { + if (ignoreAllExceptions) + { + _ = r.Exception; + return Task.CompletedTask; + } + + return r; + } + try { r.Result.Dispose(); diff --git a/Dalamud/Utility/Util.cs b/Dalamud/Utility/Util.cs index 65196b3ee..43355ac2c 100644 --- a/Dalamud/Utility/Util.cs +++ b/Dalamud/Utility/Util.cs @@ -19,7 +19,6 @@ using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility; -using Dalamud.Logging.Internal; using ImGuiNET; using Lumina.Excel.GeneratedSheets; using Serilog; @@ -638,42 +637,6 @@ public static class Util if (!Windows.Win32.PInvoke.MoveFileEx(tempPath, path, MOVE_FILE_FLAGS.MOVEFILE_REPLACE_EXISTING | MOVE_FILE_FLAGS.MOVEFILE_WRITE_THROUGH)) throw new Win32Exception(); } - - /// - /// Dispose this object. - /// - /// The object to dispose. - /// The type of object to dispose. - internal static void ExplicitDispose(this T obj) where T : IDisposable - { - 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); - } - } /// /// Gets a random, inoffensive, human-friendly string.