From 34466227cede1adca9542b13bf3b8b77693b9ed6 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:30:30 -0800 Subject: [PATCH 1/3] Add DutyState Service --- Dalamud/Game/DutyState/DutyState.cs | 181 ++++++++++++++++++ .../DutyState/DutyStateAddressResolver.cs | 23 +++ 2 files changed, 204 insertions(+) create mode 100644 Dalamud/Game/DutyState/DutyState.cs create mode 100644 Dalamud/Game/DutyState/DutyStateAddressResolver.cs diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs new file mode 100644 index 000000000..f828edad8 --- /dev/null +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -0,0 +1,181 @@ +using System; +using System.Runtime.InteropServices; + +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Hooking; +using Dalamud.IoC; +using Dalamud.IoC.Internal; +using Dalamud.Utility; + +namespace Dalamud.Game.DutyState; + +/// +/// This class represents the state of the currently occupied duty. +/// +[PluginInterface] +[InterfaceVersion("1.0")] +[ServiceManager.EarlyLoadedService] +public unsafe class DutyState : IDisposable, IServiceType +{ + private readonly DutyStateAddressResolver address; + private readonly Hook contentDirectorNetworkMessageHook; + + [ServiceManager.ServiceDependency] + private readonly Condition condition = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly Framework framework = Service.Get(); + + [ServiceManager.ServiceDependency] + private readonly ClientState.ClientState clientState = Service.Get(); + + [ServiceManager.ServiceConstructor] + private DutyState(SigScanner sigScanner) + { + this.address = new DutyStateAddressResolver(); + this.address.Setup(sigScanner); + + this.contentDirectorNetworkMessageHook = Hook.FromAddress(this.address.ContentDirectorNetworkMessage, this.ContentDirectorNetworkMessageDetour); + + this.framework.Update += this.FrameworkOnUpdateEvent; + this.clientState.TerritoryChanged += this.TerritoryOnChangedEvent; + } + + [UnmanagedFunctionPointer(CallingConvention.ThisCall)] + private delegate byte SetupContentDirectNetworkMessageDelegate(IntPtr a1, IntPtr a2, ushort* a3); + + /// + /// Event that gets fired when the duty starts. Triggers when the "Duty Start" + /// message displays, and on the remove of the ring at duty's spawn. + /// + public event EventHandler DutyStarted; + + /// + /// Event that gets fired when everyone in the party dies and the screen fades to black. + /// + public event EventHandler DutyWiped; + + /// + /// Event that gets fired when the "Duty Recommence" message displays, + /// and on the remove the the ring at duty's spawn. + /// + public event EventHandler DutyRecommenced; + + /// + /// Event that gets fired when the duty is completed successfully. + /// + public event EventHandler DutyCompleted; + + /// + /// Gets a value indicating whether the current duty has been started. + /// + public bool IsDutyStarted { get; private set; } + + /// + /// Gets or sets a value indicating whether the current duty has been completed or not. + /// Prevents DutyStarted from triggering if combat is entered after receiving a duty complete network event. + /// + private bool CompletedThisTerritory { get; set; } + + /// + /// Dispose of managed and unmanaged resources. + /// + void IDisposable.Dispose() + { + this.contentDirectorNetworkMessageHook.Dispose(); + this.framework.Update -= this.FrameworkOnUpdateEvent; + this.clientState.TerritoryChanged -= this.TerritoryOnChangedEvent; + } + + [ServiceManager.CallWhenServicesReady] + private void ContinueConstruction() + { + this.contentDirectorNetworkMessageHook.Enable(); + } + + private byte ContentDirectorNetworkMessageDetour(IntPtr a1, IntPtr a2, ushort* a3) + { + var category = *a3; + var type = *(uint*)(a3 + 4); + + // DirectorUpdate Category + if (category == 0x6D) + { + switch (type) + { + // Duty Commenced + case 0x4000_0001: + this.IsDutyStarted = true; + this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Party Wipe + case 0x4000_0005: + this.IsDutyStarted = false; + this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Duty Recommence + case 0x4000_0006: + this.IsDutyStarted = true; + this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Duty Completed + case 0x4000_0003: + this.IsDutyStarted = false; + this.CompletedThisTerritory = true; + this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); + break; + } + } + + return this.contentDirectorNetworkMessageHook.Original(a1, a2, a3); + } + + private void TerritoryOnChangedEvent(object? sender, ushort e) + { + if (this.IsDutyStarted) + { + this.IsDutyStarted = false; + } + + this.CompletedThisTerritory = false; + } + + /// + /// Fallback event handler in the case that we missed the duty started event. + /// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event. + /// + /// Framework reference. + private void FrameworkOnUpdateEvent(Framework framework1) + { + // If the duty hasn't been started, and has not been completed yet this territory + if (!this.IsDutyStarted && !this.CompletedThisTerritory) + { + // If the player is in a duty, and got into combat, we need to set the duty stated value + if (this.IsBoundByDuty() && this.IsInCombat()) + { + this.IsDutyStarted = true; + } + } + + // If the player is no longer bound by duty but we missed the event somehow, set it to false + else if (!this.IsBoundByDuty() && this.IsDutyStarted) + { + this.IsDutyStarted = false; + + // Could potentially add a call to DutyCompleted here since this + // should only be reached if we are actually no longer in a duty, and missed the network event. + } + } + + private bool IsBoundByDuty() + { + return this.condition[ConditionFlag.BoundByDuty] || + this.condition[ConditionFlag.BoundByDuty56] || + this.condition[ConditionFlag.BoundByDuty95]; + } + + private bool IsInCombat() => this.condition[ConditionFlag.InCombat]; +} diff --git a/Dalamud/Game/DutyState/DutyStateAddressResolver.cs b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs new file mode 100644 index 000000000..801e5ef55 --- /dev/null +++ b/Dalamud/Game/DutyState/DutyStateAddressResolver.cs @@ -0,0 +1,23 @@ +using System; + +namespace Dalamud.Game.DutyState; + +/// +/// Duty state memory address resolver. +/// +public class DutyStateAddressResolver : BaseAddressResolver +{ + /// + /// Gets the address of the method which is called when the client receives a content director update. + /// + public IntPtr ContentDirectorNetworkMessage { get; private set; } + + /// + /// Scan for and setup any configured address pointers. + /// + /// The signature scanner to facilitate setup. + protected override void Setup64Bit(SigScanner sig) + { + this.ContentDirectorNetworkMessage = sig.ScanText("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08"); + } +} From f8919da11f1562321df54e986167afd649d09357 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:49:41 -0800 Subject: [PATCH 2/3] Add DutyState to Self-Test --- .../SelfTest/AgingSteps/DutyStateAgingStep.cs | 52 +++++++++++++++++++ .../Windows/SelfTest/SelfTestWindow.cs | 1 + 2 files changed, 53 insertions(+) create mode 100644 Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/DutyStateAgingStep.cs diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/DutyStateAgingStep.cs b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/DutyStateAgingStep.cs new file mode 100644 index 000000000..2a92d7bd3 --- /dev/null +++ b/Dalamud/Interface/Internal/Windows/SelfTest/AgingSteps/DutyStateAgingStep.cs @@ -0,0 +1,52 @@ +using Dalamud.Game.DutyState; +using ImGuiNET; + +namespace Dalamud.Interface.Internal.Windows.SelfTest.AgingSteps; + +/// +/// Test setup for the DutyState service class. +/// +internal class DutyStateAgingStep : IAgingStep +{ + private bool subscribed = false; + private bool hasPassed = false; + + /// + public string Name => "Test DutyState"; + + /// + public SelfTestStepResult RunStep() + { + var dutyState = Service.Get(); + + ImGui.Text("Enter a duty now..."); + + if (!this.subscribed) + { + dutyState.DutyStarted += this.DutyStateOnDutyStarted; + this.subscribed = true; + } + + if (this.hasPassed) + { + dutyState.DutyStarted -= this.DutyStateOnDutyStarted; + this.subscribed = false; + return SelfTestStepResult.Pass; + } + + return SelfTestStepResult.Waiting; + } + + /// + public void CleanUp() + { + var dutyState = Service.Get(); + + dutyState.DutyStarted -= this.DutyStateOnDutyStarted; + } + + private void DutyStateOnDutyStarted(object? sender, ushort e) + { + this.hasPassed = true; + } +} diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 352c5d322..33a3e57cd 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -42,6 +42,7 @@ internal class SelfTestWindow : Window new PartyFinderAgingStep(), new HandledExceptionAgingStep(), new LogoutEventAgingStep(), + new DutyStateAgingStep(), }; private readonly List<(SelfTestStepResult Result, TimeSpan? Duration)> stepResults = new(); From 163001ec33f737b78dd2320cb2a5e4fa9656d5e5 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Thu, 26 Jan 2023 14:52:48 -0800 Subject: [PATCH 3/3] Move DutyState test to before logout --- Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs index 33a3e57cd..17d944872 100644 --- a/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs +++ b/Dalamud/Interface/Internal/Windows/SelfTest/SelfTestWindow.cs @@ -41,8 +41,8 @@ internal class SelfTestWindow : Window new LuminaAgingStep(), new PartyFinderAgingStep(), new HandledExceptionAgingStep(), - new LogoutEventAgingStep(), new DutyStateAgingStep(), + new LogoutEventAgingStep(), }; private readonly List<(SelfTestStepResult Result, TimeSpan? Duration)> stepResults = new();