Add events for ClassJob and Level change (#1985)

* Add events for ClassJob and Level change
* Correctly handle uiModuleHandlePacketHook
* Update Dalamud/Plugin/Services/IClientState.cs


Co-authored-by: KazWolfe <KazWolfe@users.noreply.github.com>
This commit is contained in:
Haselnussbomber 2024-07-30 02:17:07 +02:00 committed by GitHub
parent 7369878789
commit 9bb9b3acf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 124 additions and 26 deletions

View file

@ -13,7 +13,10 @@ using Dalamud.IoC.Internal;
using Dalamud.Logging.Internal; using Dalamud.Logging.Internal;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.Game.Event;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
@ -29,10 +32,11 @@ namespace Dalamud.Game.ClientState;
internal sealed class ClientState : IInternalDisposableService, IClientState internal sealed class ClientState : IInternalDisposableService, IClientState
{ {
private static readonly ModuleLog Log = new("ClientState"); private static readonly ModuleLog Log = new("ClientState");
private readonly GameLifecycle lifecycle; private readonly GameLifecycle lifecycle;
private readonly ClientStateAddressResolver address; private readonly ClientStateAddressResolver address;
private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook; private readonly Hook<SetupTerritoryTypeDelegate> setupTerritoryTypeHook;
private readonly Hook<UIModule.Delegates.HandlePacket> uiModuleHandlePacketHook;
[ServiceManager.ServiceDependency] [ServiceManager.ServiceDependency]
private readonly Framework framework = Service<Framework>.Get(); private readonly Framework framework = Service<Framework>.Get();
@ -44,7 +48,7 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
private bool lastFramePvP; private bool lastFramePvP;
[ServiceManager.ServiceConstructor] [ServiceManager.ServiceConstructor]
private ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle)
{ {
this.lifecycle = lifecycle; this.lifecycle = lifecycle;
this.address = new ClientStateAddressResolver(); this.address = new ClientStateAddressResolver();
@ -57,20 +61,28 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}"); Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(this.address.SetupTerritoryType)}");
this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour); this.setupTerritoryTypeHook = Hook<SetupTerritoryTypeDelegate>.FromAddress(this.address.SetupTerritoryType, this.SetupTerritoryTypeDetour);
this.uiModuleHandlePacketHook = Hook<UIModule.Delegates.HandlePacket>.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour);
this.framework.Update += this.FrameworkOnOnUpdateEvent; this.framework.Update += this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop;
this.setupTerritoryTypeHook.Enable(); this.setupTerritoryTypeHook.Enable();
this.uiModuleHandlePacketHook.Enable();
} }
[UnmanagedFunctionPointer(CallingConvention.ThisCall)] [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
private delegate IntPtr SetupTerritoryTypeDelegate(IntPtr manager, ushort terriType); private unsafe delegate void SetupTerritoryTypeDelegate(EventFramework* eventFramework, ushort terriType);
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ushort>? TerritoryChanged; public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Login; public event Action? Login;
@ -124,25 +136,25 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
/// Gets client state address resolver. /// Gets client state address resolver.
/// </summary> /// </summary>
internal ClientStateAddressResolver AddressResolver => this.address; internal ClientStateAddressResolver AddressResolver => this.address;
/// <inheritdoc/> /// <inheritdoc/>
public bool IsClientIdle(out ConditionFlag blockingFlag) public bool IsClientIdle(out ConditionFlag blockingFlag)
{ {
blockingFlag = 0; blockingFlag = 0;
if (this.LocalPlayer is null) return true; if (this.LocalPlayer is null) return true;
var condition = Service<Conditions.Condition>.GetNullable(); var condition = Service<Conditions.Condition>.GetNullable();
var blockingConditions = condition.AsReadOnlySet().Except([ var blockingConditions = condition.AsReadOnlySet().Except([
ConditionFlag.NormalConditions, ConditionFlag.NormalConditions,
ConditionFlag.Jumping, ConditionFlag.Jumping,
ConditionFlag.Mounted, ConditionFlag.Mounted,
ConditionFlag.UsingParasol]); ConditionFlag.UsingParasol]);
blockingFlag = blockingConditions.FirstOrDefault(); blockingFlag = blockingConditions.FirstOrDefault();
return blockingFlag == 0; return blockingFlag == 0;
} }
/// <inheritdoc/> /// <inheritdoc/>
public bool IsClientIdle() => this.IsClientIdle(out _); public bool IsClientIdle() => this.IsClientIdle(out _);
@ -152,18 +164,66 @@ internal sealed class ClientState : IInternalDisposableService, IClientState
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.setupTerritoryTypeHook.Dispose(); this.setupTerritoryTypeHook.Dispose();
this.uiModuleHandlePacketHook.Dispose();
this.framework.Update -= this.FrameworkOnOnUpdateEvent; this.framework.Update -= this.FrameworkOnOnUpdateEvent;
this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop;
} }
private IntPtr SetupTerritoryTypeDetour(IntPtr manager, ushort terriType) private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType)
{ {
this.TerritoryType = terriType; this.TerritoryType = territoryType;
this.TerritoryChanged?.InvokeSafely(terriType); 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<IClientState.ClassJobChangeDelegate>())
{
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<IClientState.LevelChangeDelegate>())
{
try
{
action(classJobId, level);
}
catch (Exception ex)
{
Log.Error(ex, "Exception during raise of {handler}", action.Method);
}
}
break;
}
}
} }
private void NetworkHandlersOnCfPop(ContentFinderCondition e) private void NetworkHandlersOnCfPop(ContentFinderCondition e)
@ -240,28 +300,36 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
internal ClientStatePluginScoped() internal ClientStatePluginScoped()
{ {
this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; this.clientStateService.TerritoryChanged += this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged += this.ClassJobChangedForward;
this.clientStateService.LevelChanged += this.LevelChangedForward;
this.clientStateService.Login += this.LoginForward; this.clientStateService.Login += this.LoginForward;
this.clientStateService.Logout += this.LogoutForward; this.clientStateService.Logout += this.LogoutForward;
this.clientStateService.EnterPvP += this.EnterPvPForward; this.clientStateService.EnterPvP += this.EnterPvPForward;
this.clientStateService.LeavePvP += this.ExitPvPForward; this.clientStateService.LeavePvP += this.ExitPvPForward;
this.clientStateService.CfPop += this.ContentFinderPopForward; this.clientStateService.CfPop += this.ContentFinderPopForward;
} }
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ushort>? TerritoryChanged; public event Action<ushort>? TerritoryChanged;
/// <inheritdoc/>
public event IClientState.ClassJobChangeDelegate? ClassJobChanged;
/// <inheritdoc/>
public event IClientState.LevelChangeDelegate? LevelChanged;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Login; public event Action? Login;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? Logout; public event Action? Logout;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? EnterPvP; public event Action? EnterPvP;
/// <inheritdoc/> /// <inheritdoc/>
public event Action? LeavePvP; public event Action? LeavePvP;
/// <inheritdoc/> /// <inheritdoc/>
public event Action<ContentFinderCondition>? CfPop; public event Action<ContentFinderCondition>? CfPop;
@ -270,7 +338,7 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
/// <inheritdoc/> /// <inheritdoc/>
public ushort TerritoryType => this.clientStateService.TerritoryType; public ushort TerritoryType => this.clientStateService.TerritoryType;
/// <inheritdoc/> /// <inheritdoc/>
public uint MapId => this.clientStateService.MapId; public uint MapId => this.clientStateService.MapId;
@ -302,6 +370,8 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat
void IInternalDisposableService.DisposeService() void IInternalDisposableService.DisposeService()
{ {
this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward;
this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward;
this.clientStateService.LevelChanged -= this.LevelChangedForward;
this.clientStateService.Login -= this.LoginForward; this.clientStateService.Login -= this.LoginForward;
this.clientStateService.Logout -= this.LogoutForward; this.clientStateService.Logout -= this.LogoutForward;
this.clientStateService.EnterPvP -= this.EnterPvPForward; 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 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 LoginForward() => this.Login?.Invoke();
private void LogoutForward() => this.Logout?.Invoke(); private void LogoutForward() => this.Logout?.Invoke();
private void EnterPvPForward() => this.EnterPvP?.Invoke(); private void EnterPvPForward() => this.EnterPvP?.Invoke();
private void ExitPvPForward() => this.LeavePvP?.Invoke(); private void ExitPvPForward() => this.LeavePvP?.Invoke();
private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc); private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc);

View file

@ -9,11 +9,35 @@ namespace Dalamud.Plugin.Services;
/// </summary> /// </summary>
public interface IClientState public interface IClientState
{ {
/// <summary>
/// A delegate type used for the <see cref="ClassJobChanged"/> event.
/// </summary>
/// <param name="classJobId">The new ClassJob id.</param>
public delegate void ClassJobChangeDelegate(uint classJobId);
/// <summary>
/// A delegate type used for the <see cref="LevelChanged"/> event.
/// </summary>
/// <param name="classJobId">The ClassJob id.</param>
/// <param name="level">The level of the corresponding ClassJob.</param>
public delegate void LevelChangeDelegate(uint classJobId, uint level);
/// <summary> /// <summary>
/// Event that gets fired when the current Territory changes. /// Event that gets fired when the current Territory changes.
/// </summary> /// </summary>
public event Action<ushort> TerritoryChanged; public event Action<ushort> TerritoryChanged;
/// <summary>
/// Event that fires when a characters ClassJob changed.
/// </summary>
public event ClassJobChangeDelegate? ClassJobChanged;
/// <summary>
/// Event that fires when <em>any</em> character level changes, including levels
/// for a not-currently-active ClassJob (e.g. PvP matches, DoH/DoL).
/// </summary>
public event LevelChangeDelegate? LevelChanged;
/// <summary> /// <summary>
/// Event that fires when a character is logging in, and the local character object is available. /// Event that fires when a character is logging in, and the local character object is available.
/// </summary> /// </summary>