From 494ea5eb9f8899f6e8fc00e9ffb5bf161771ebd7 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:48:57 -0800 Subject: [PATCH 1/3] Add DutyState Service Class --- Dalamud/Game/DutyState/DutyState.cs | 175 ++++++++++++++++++ .../DutyState/DutyStateAddressResolver.cs | 23 +++ 2 files changed, 198 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..2ae122f13 --- /dev/null +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -0,0 +1,175 @@ +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.BlockingEarlyLoadedService] +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 0x40000001: + this.IsDutyStarted = true; + this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Party Wipe + case 0x40000005: + this.IsDutyStarted = false; + this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Duty Recommence + case 0x40000006: + this.IsDutyStarted = true; + this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); + break; + + // Duty Completed + case 0x40000003: + 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) + { + } + + /// + /// 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..ce3c1a7e6 --- /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.GetStaticAddressFromSig("48 89 5C 24 ?? 48 89 74 24 ?? 57 48 83 EC ?? 48 8B D9 49 8B F8 41 0F B7 08"); + } +} From 7d6432da055fdf531f537e85c2bbf4b927988077 Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 24 Jan 2023 15:51:32 -0800 Subject: [PATCH 2/3] Add missing TerritoryChange Code --- Dalamud/Game/DutyState/DutyState.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index 2ae122f13..eb7063d9a 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -135,6 +135,12 @@ public unsafe class DutyState : IDisposable, IServiceType private void TerritoryOnChangedEvent(object? sender, ushort e) { + if (this.IsDutyStarted) + { + this.IsDutyStarted = false; + } + + this.CompletedThisTerritory = false; } /// From 6f4c6514a4be0beb078c8aca5ab8b4fe600515ce Mon Sep 17 00:00:00 2001 From: MidoriKami <9083275+MidoriKami@users.noreply.github.com> Date: Tue, 24 Jan 2023 16:54:00 -0800 Subject: [PATCH 3/3] Use EarlyLoadedService instead of BlockingEarlyLoadedService --- Dalamud/Game/DutyState/DutyState.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs index eb7063d9a..336daf2bc 100644 --- a/Dalamud/Game/DutyState/DutyState.cs +++ b/Dalamud/Game/DutyState/DutyState.cs @@ -14,7 +14,7 @@ namespace Dalamud.Game.DutyState; /// [PluginInterface] [InterfaceVersion("1.0")] -[ServiceManager.BlockingEarlyLoadedService] +[ServiceManager.EarlyLoadedService] public unsafe class DutyState : IDisposable, IServiceType { private readonly DutyStateAddressResolver address; @@ -104,25 +104,25 @@ public unsafe class DutyState : IDisposable, IServiceType switch (type) { // Duty Commenced - case 0x40000001: + case 0x4000_0001: this.IsDutyStarted = true; this.DutyStarted.InvokeSafely(this, this.clientState.TerritoryType); break; // Party Wipe - case 0x40000005: + case 0x4000_0005: this.IsDutyStarted = false; this.DutyWiped.InvokeSafely(this, this.clientState.TerritoryType); break; // Duty Recommence - case 0x40000006: + case 0x4000_0006: this.IsDutyStarted = true; this.DutyRecommenced.InvokeSafely(this, this.clientState.TerritoryType); break; // Duty Completed - case 0x40000003: + case 0x4000_0003: this.IsDutyStarted = false; this.CompletedThisTerritory = true; this.DutyCompleted.InvokeSafely(this, this.clientState.TerritoryType); @@ -145,9 +145,9 @@ public unsafe class DutyState : IDisposable, IServiceType /// /// 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 + /// Joining a duty in progress, or disconnecting and reconnecting will cause the player to miss the event. /// - /// Framework reference + /// Framework reference. private void FrameworkOnUpdateEvent(Framework framework1) { // If the duty hasn't been started, and has not been completed yet this territory