diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index b31bcf780..edf5e7ff9 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -13,7 +13,10 @@ using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; + using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Event; +using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.GeneratedSheets; @@ -29,10 +32,11 @@ namespace Dalamud.Game.ClientState; internal sealed class ClientState : IInternalDisposableService, IClientState { private static readonly ModuleLog Log = new("ClientState"); - + private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; private readonly Hook setupTerritoryTypeHook; + private readonly Hook uiModuleHandlePacketHook; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -44,7 +48,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private bool lastFramePvP; [ServiceManager.ServiceConstructor] - private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) + private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) { this.lifecycle = lifecycle; this.address = new ClientStateAddressResolver(); @@ -57,20 +61,28 @@ internal sealed class ClientState : IInternalDisposableService, IClientState Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}"); this.setupTerritoryTypeHook = Hook.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); + this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.setupTerritoryTypeHook.Enable(); + this.uiModuleHandlePacketHook.Enable(); } [UnmanagedFunctionPointer(CallingConvention.ThisCall)] - private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType); + private unsafe delegate void SetupTerritoryTypeDelegate(EventFramework* eventFramework, ushort terriType); /// public event Action? TerritoryChanged; + /// + public event IClientState.ClassJobChangeDelegate? ClassJobChanged; + + /// + public event IClientState.LevelChangeDelegate? LevelChanged; + /// public event Action? Login; @@ -124,25 +136,25 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// Gets client state address resolver. /// internal ClientStateAddressResolver AddressResolver => this.address; - + /// public bool IsClientIdle(out ConditionFlag blockingFlag) { blockingFlag = 0; if (this.LocalPlayer is null) return true; - + var condition = Service.GetNullable(); - + var blockingConditions = condition.AsReadOnlySet().Except([ - ConditionFlag.NormalConditions, - ConditionFlag.Jumping, - ConditionFlag.Mounted, + ConditionFlag.NormalConditions, + ConditionFlag.Jumping, + ConditionFlag.Mounted, ConditionFlag.UsingParasol]); blockingFlag = blockingConditions.FirstOrDefault(); return blockingFlag == 0; } - + /// public bool IsClientIdle() => this.IsClientIdle(out _); @@ -152,18 +164,66 @@ internal sealed class ClientState : IInternalDisposableService, IClientState void IInternalDisposableService.DisposeService() { this.setupTerritoryTypeHook.Dispose(); + this.uiModuleHandlePacketHook.Dispose(); this.framework.Update -= this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } - private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) + private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType) { - this.TerritoryType = terriType; - this.TerritoryChanged?.InvokeSafely(terriType); + this.TerritoryType = territoryType; + this.TerritoryChanged?.InvokeSafely(territoryType); - Log.Debug("TerritoryType changed: {0}", terriType); + Log.Debug("TerritoryType changed: {0}", territoryType); - return this.setupTerritoryTypeHook.Original(manager, terriType); + this.setupTerritoryTypeHook.Original(eventFramework, territoryType); + } + + private unsafe void UIModuleHandlePacketDetour(UIModule* thisPtr, UIModulePacketType type, uint uintParam, void* packet) + { + this.uiModuleHandlePacketHook!.Original(thisPtr, type, uintParam, packet); + + switch (type) + { + case UIModulePacketType.ClassJobChange: + { + var classJobId = uintParam; + + foreach (var action in this.ClassJobChanged.GetInvocationList().Cast()) + { + try + { + action(classJobId); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); + } + } + + break; + } + + case UIModulePacketType.LevelChange: + { + var classJobId = *(uint*)packet; + var level = *(ushort*)((nint)packet + 4); + + foreach (var action in this.LevelChanged.GetInvocationList().Cast()) + { + try + { + action(classJobId, level); + } + catch (Exception ex) + { + Log.Error(ex, "Exception during raise of {handler}", action.Method); + } + } + + break; + } + } } private void NetworkHandlersOnCfPop(ContentFinderCondition e) @@ -240,28 +300,36 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat internal ClientStatePluginScoped() { this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; + this.clientStateService.ClassJobChanged += this.ClassJobChangedForward; + this.clientStateService.LevelChanged += this.LevelChangedForward; 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 IClientState.ClassJobChangeDelegate? ClassJobChanged; + + /// + public event IClientState.LevelChangeDelegate? LevelChanged; + /// public event Action? Login; - + /// public event Action? Logout; - + /// public event Action? EnterPvP; - + /// public event Action? LeavePvP; - + /// public event Action? CfPop; @@ -270,7 +338,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// public ushort TerritoryType => this.clientStateService.TerritoryType; - + /// public uint MapId => this.clientStateService.MapId; @@ -302,6 +370,8 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat void IInternalDisposableService.DisposeService() { this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; + this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward; + this.clientStateService.LevelChanged -= this.LevelChangedForward; this.clientStateService.Login -= this.LoginForward; this.clientStateService.Logout -= this.LogoutForward; this.clientStateService.EnterPvP -= this.EnterPvPForward; @@ -317,13 +387,17 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat } private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); - + + private void ClassJobChangedForward(uint classJobId) => this.ClassJobChanged?.Invoke(classJobId); + + private void LevelChangedForward(uint classJobId, uint level) => this.LevelChanged?.Invoke(classJobId, level); + 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/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index db4903178..6b0555f23 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -9,11 +9,35 @@ namespace Dalamud.Plugin.Services; /// public interface IClientState { + /// + /// A delegate type used for the event. + /// + /// The new ClassJob id. + public delegate void ClassJobChangeDelegate(uint classJobId); + + /// + /// A delegate type used for the event. + /// + /// The ClassJob id. + /// The level of the corresponding ClassJob. + public delegate void LevelChangeDelegate(uint classJobId, uint level); + /// /// Event that gets fired when the current Territory changes. /// public event Action TerritoryChanged; + /// + /// Event that fires when a characters ClassJob changed. + /// + public event ClassJobChangeDelegate? ClassJobChanged; + + /// + /// Event that fires when any character level changes, including levels + /// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL). + /// + public event LevelChangeDelegate? LevelChanged; + /// /// Event that fires when a character is logging in, and the local character object is available. ///