diff --git a/Dalamud/Game/DutyState/DutyState.cs b/Dalamud/Game/DutyState/DutyState.cs new file mode 100644 index 000000000..336daf2bc --- /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..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"); + } +}