From b742abe77fd0bde0d5cf5657ec28759989bb370c Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 21 Sep 2023 21:55:16 -0700 Subject: [PATCH] Add ClientStatePluginScoped (#1384) * Add ClientStatePluginScoped * Restore InvokeSafely * Add InvokeSafely for basic Action types. * Set delegates to null to prevent leaking memory * Resolve Merge --- Dalamud/Game/ClientState/ClientState.cs | 137 +++++++++++++++--- Dalamud/Game/DutyState/DutyState.cs | 2 +- .../Game/Network/Internal/NetworkHandlers.cs | 6 +- .../AgingSteps/EnterTerritoryAgingStep.cs | 4 +- .../AgingSteps/LoginEventAgingStep.cs | 2 +- .../AgingSteps/LogoutEventAgingStep.cs | 2 +- Dalamud/Plugin/Services/IClientState.cs | 8 +- Dalamud/Utility/EventHandlerExtensions.cs | 41 +++++- 8 files changed, 165 insertions(+), 37 deletions(-) diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index cef802c81..baf6f6634 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -8,24 +8,25 @@ using Dalamud.Game.Network.Internal; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; +using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Client.Game; -using Serilog; +using Lumina.Excel.GeneratedSheets; + +using Action = System.Action; namespace Dalamud.Game.ClientState; /// /// This class represents the state of the game client at the time of access. /// -[PluginInterface] [InterfaceVersion("1.0")] [ServiceManager.BlockingEarlyLoadedService] -#pragma warning disable SA1015 -[ResolveVia] -#pragma warning restore SA1015 internal sealed class ClientState : IDisposable, IServiceType, IClientState { + private static readonly ModuleLog Log = new("ClientState"); + private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; @@ -37,7 +38,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private readonly NetworkHandlers networkHandlers = Service.Get(); private bool lastConditionNone = true; - private bool lastFramePvP = false; + private bool lastFramePvP; [ServiceManager.ServiceConstructor] private ClientState(SigScanner sigScanner, DalamudStartInfo startInfo, GameLifecycle lifecycle) @@ -63,22 +64,22 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType); /// - public event EventHandler TerritoryChanged; + public event Action? TerritoryChanged; /// - public event EventHandler Login; + public event Action? Login; /// - public event EventHandler Logout; + public event Action? Logout; /// - public event Action EnterPvP; + public event Action? EnterPvP; /// - public event Action LeavePvP; + public event Action? LeavePvP; /// - public event EventHandler CfPop; + public event Action? CfPop; /// public ClientLanguage ClientLanguage { get; } @@ -128,16 +129,16 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) { this.TerritoryType = terriType; - this.TerritoryChanged?.InvokeSafely(this, terriType); + this.TerritoryChanged?.InvokeSafely(terriType); Log.Debug("TerritoryType changed: {0}", terriType); return this.setupTerritoryTypeHook.Original(manager, terriType); } - private void NetworkHandlersOnCfPop(object sender, Lumina.Excel.GeneratedSheets.ContentFinderCondition e) + private void NetworkHandlersOnCfPop(ContentFinderCondition e) { - this.CfPop?.InvokeSafely(this, e); + this.CfPop?.InvokeSafely(e); } private void FrameworkOnOnUpdateEvent(IFramework framework1) @@ -149,12 +150,12 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState if (condition == null || gameGui == null || data == null) return; - if (condition.Any() && this.lastConditionNone == true && this.LocalPlayer != null) + if (condition.Any() && this.lastConditionNone && this.LocalPlayer != null) { Log.Debug("Is login"); this.lastConditionNone = false; this.IsLoggedIn = true; - this.Login?.InvokeSafely(this, null); + this.Login?.InvokeSafely(); gameGui.ResetUiHideState(); this.lifecycle.ResetLogout(); @@ -165,7 +166,7 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState Log.Debug("Is logout"); this.lastConditionNone = true; this.IsLoggedIn = false; - this.Logout?.InvokeSafely(this, null); + this.Logout?.InvokeSafely(); gameGui.ResetUiHideState(); this.lifecycle.SetLogout(); @@ -189,3 +190,103 @@ internal sealed class ClientState : IDisposable, IServiceType, IClientState } } } + +/// +/// Plugin-scoped version of a GameConfig service. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.ScopedService] +#pragma warning disable SA1015 +[ResolveVia] +#pragma warning restore SA1015 +internal class ClientStatePluginScoped : IDisposable, IServiceType, IClientState +{ + [ServiceManager.ServiceDependency] + private readonly ClientState clientStateService = Service.Get(); + + /// + /// Initializes a new instance of the class. + /// + internal ClientStatePluginScoped() + { + this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; + this.clientStateService.Login += this.LoginForward; + this.clientStateService.Logout += this.LogoutForward; + this.clientStateService.EnterPvP += this.EnterPvPForward; + this.clientStateService.LeavePvP += this.ExitPvPForward; + this.clientStateService.CfPop += this.ContentFinderPopForward; + } + + /// + public event Action? TerritoryChanged; + + /// + public event Action? Login; + + /// + public event Action? Logout; + + /// + public event Action? EnterPvP; + + /// + public event Action? LeavePvP; + + /// + public event Action? CfPop; + + /// + public ClientLanguage ClientLanguage => this.clientStateService.ClientLanguage; + + /// + public ushort TerritoryType => this.clientStateService.TerritoryType; + + /// + public PlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; + + /// + public ulong LocalContentId => this.clientStateService.LocalContentId; + + /// + public bool IsLoggedIn => this.clientStateService.IsLoggedIn; + + /// + public bool IsPvP => this.clientStateService.IsPvP; + + /// + public bool IsPvPExcludingDen => this.clientStateService.IsPvPExcludingDen; + + /// + public bool IsGPosing => this.clientStateService.IsGPosing; + + /// + public void Dispose() + { + this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; + this.clientStateService.Login -= this.LoginForward; + this.clientStateService.Logout -= this.LogoutForward; + this.clientStateService.EnterPvP -= this.EnterPvPForward; + this.clientStateService.LeavePvP -= this.ExitPvPForward; + this.clientStateService.CfPop -= this.ContentFinderPopForward; + + this.TerritoryChanged = null; + this.Login = null; + this.Logout = null; + this.EnterPvP = null; + this.LeavePvP = null; + this.CfPop = null; + } + + private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); + + private void LoginForward() => this.Login?.Invoke(); + + private void LogoutForward() => this.Logout?.Invoke(); + + private void EnterPvPForward() => this.EnterPvP?.Invoke(); + + private void ExitPvPForward() => this.LeavePvP?.Invoke(); + + private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc); +} diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index c52ceff0f..3890a1f8b 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -120,7 +120,7 @@ internal unsafe class DutyState : IDisposable, IServiceType, IDutyState return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3); } - private void TerritoryOnChangedEvent(object? sender, ushort e) + private void TerritoryOnChangedEvent(ushort territoryId) { if (this.IsDutyStarted) { diff --git a/Dalamud/Game/Network/Internal/NetworkHandlers.cs b/Dalamud/Game/Network/Internal/NetworkHandlers.cs index 1ccf6c6d5..77bf99c1b 100644 --- a/Dalamud/Game/Network/Internal/NetworkHandlers.cs +++ b/Dalamud/Game/Network/Internal/NetworkHandlers.cs @@ -44,7 +44,7 @@ internal class NetworkHandlers : IDisposable, IServiceType private NetworkHandlers(GameNetwork gameNetwork) { this.uploader = new UniversalisMarketBoardUploader(); - this.CfPop = (_, _) => { }; + this.CfPop = _ => { }; this.messages = Observable.Create(observer => { @@ -75,7 +75,7 @@ internal class NetworkHandlers : IDisposable, IServiceType /// /// Event which gets fired when a duty is ready. /// - public event EventHandler CfPop; + public event Action CfPop; /// /// Disposes of managed and unmanaged resources. @@ -430,7 +430,7 @@ internal class NetworkHandlers : IDisposable, IServiceType Service.GetNullable()?.Print($"Duty pop: {cfcName}"); } - this.CfPop.InvokeSafely(this, cfCondition); + this.CfPop.InvokeSafely(cfCondition); }).ContinueWith( task => Log.Error(task.Exception, "CfPop.Invoke failed"), TaskContinuationOptions.OnlyOnFaulted); diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs index d301cb1ff..4f5c758d6 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/EnterTerritoryAgingStep.cs @@ -59,9 +59,9 @@ internal class EnterTerritoryAgingStep : IAgingStep this.subscribed = false; } - private void ClientStateOnTerritoryChanged(object sender, ushort e) + private void ClientStateOnTerritoryChanged(ushort territoryId) { - if (e == this.territory) + if (territoryId == this.territory) { this.hasPassed = true; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs index c1dba389f..23b0b903a 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LoginEventAgingStep.cs @@ -51,7 +51,7 @@ internal class LoginEventAgingStep : IAgingStep } } - private void ClientStateOnOnLogin(object sender, EventArgs e) + private void ClientStateOnOnLogin() { this.hasPassed = true; } diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs index 060c0bcc8..c4c6ebfce 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/LogoutEventAgingStep.cs @@ -51,7 +51,7 @@ internal class LogoutEventAgingStep : IAgingStep } } - private void ClientStateOnOnLogout(object sender, EventArgs e) + private void ClientStateOnOnLogout() { this.hasPassed = true; } diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 881cad841..652a6c888 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -12,17 +12,17 @@ public interface IClientState /// /// Event that gets fired when the current Territory changes. /// - public event EventHandler TerritoryChanged; + public event Action TerritoryChanged; /// /// Event that fires when a character is logging in, and the local character object is available. /// - public event EventHandler Login; + public event Action Login; /// /// Event that fires when a character is logging out. /// - public event EventHandler Logout; + public event Action Logout; /// /// Event that fires when a character is entering PvP. @@ -37,7 +37,7 @@ public interface IClientState /// /// Event that gets fired when a duty is ready. /// - public event EventHandler CfPop; + public event Action CfPop; /// /// Gets the language of the client. diff --git a/Dalamud/Utility/EventHandlerExtensions.cs b/Dalamud/Utility/EventHandlerExtensions.cs index eefd245bb..d05ad6ea5 100644 --- a/Dalamud/Utility/EventHandlerExtensions.cs +++ b/Dalamud/Utility/EventHandlerExtensions.cs @@ -1,12 +1,9 @@ -using System; using System.Linq; using Dalamud.Game; using Dalamud.Plugin.Services; using Serilog; -using static Dalamud.Game.Framework; - namespace Dalamud.Utility; /// @@ -21,7 +18,7 @@ internal static class EventHandlerExtensions /// The EventHandler in question. /// Default sender for Invoke equivalent. /// Default EventArgs for Invoke equivalent. - public static void InvokeSafely(this EventHandler eh, object sender, EventArgs e) + public static void InvokeSafely(this EventHandler? eh, object sender, EventArgs e) { if (eh == null) return; @@ -40,7 +37,7 @@ internal static class EventHandlerExtensions /// Default sender for Invoke equivalent. /// Default EventArgs for Invoke equivalent. /// Type of EventArgs. - public static void InvokeSafely(this EventHandler eh, object sender, T e) + public static void InvokeSafely(this EventHandler? eh, object sender, T e) { if (eh == null) return; @@ -56,7 +53,7 @@ internal static class EventHandlerExtensions /// of a thrown Exception inside of an invocation. /// /// The Action in question. - public static void InvokeSafely(this Action act) + public static void InvokeSafely(this Action? act) { if (act == null) return; @@ -67,13 +64,31 @@ internal static class EventHandlerExtensions } } + /// + /// Replacement for Invoke() on event Actions to catch exceptions that stop event propagation in case + /// of a thrown Exception inside of an invocation. + /// + /// The Action in question. + /// Templated argument for Action. + /// Type of Action args. + public static void InvokeSafely(this Action? act, T argument) + { + if (act == null) + return; + + foreach (var action in act.GetInvocationList().Cast>()) + { + HandleInvoke(action, argument); + } + } + /// /// Replacement for Invoke() on OnUpdateDelegate to catch exceptions that stop event propagation in case /// of a thrown Exception inside of an invocation. /// /// The OnUpdateDelegate in question. /// Framework to be passed on to OnUpdateDelegate. - public static void InvokeSafely(this IFramework.OnUpdateDelegate updateDelegate, Framework framework) + public static void InvokeSafely(this IFramework.OnUpdateDelegate? updateDelegate, Framework framework) { if (updateDelegate == null) return; @@ -95,4 +110,16 @@ internal static class EventHandlerExtensions Log.Error(ex, "Exception during raise of {handler}", act.Method); } } + + private static void HandleInvoke(Action act, T argument) + { + try + { + act(argument); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", act.Method); + } + } }