From 9091216e1c5b32097224e9a57c088b959589d47c Mon Sep 17 00:00:00 2001 From: Haselnussbomber Date: Mon, 29 Sep 2025 18:02:53 +0200 Subject: [PATCH] Update ClientState (#2410) * Add MapChanged event to ClientState * Add PublicInstanceId with event to ClientState * Set eventhandlers to null * Rework events and add ZoneInit event --- Dalamud/Game/ClientState/ClientState.cs | 214 ++++++++++++++---- .../ClientState/ClientStateAddressResolver.cs | 16 +- Dalamud/Game/ClientState/ZoneInit.cs | 90 ++++++++ Dalamud/Plugin/Services/IClientState.cs | 21 ++ 4 files changed, 286 insertions(+), 55 deletions(-) create mode 100644 Dalamud/Game/ClientState/ZoneInit.cs diff --git a/Dalamud/Game/ClientState/ClientState.cs b/Dalamud/Game/ClientState/ClientState.cs index 407b54060..e92af21c3 100644 --- a/Dalamud/Game/ClientState/ClientState.cs +++ b/Dalamud/Game/ClientState/ClientState.cs @@ -15,8 +15,8 @@ using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -36,9 +36,9 @@ internal sealed class ClientState : IInternalDisposableService, IClientState private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; - private readonly Hook setupTerritoryTypeHook; + private readonly Hook handleZoneInitPacketHook; private readonly Hook uiModuleHandlePacketHook; - private Hook onLogoutHook; + private readonly Hook setCurrentInstanceHook; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); @@ -46,6 +46,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState [ServiceManager.ServiceDependency] private readonly NetworkHandlers networkHandlers = Service.Get(); + private Hook onLogoutHook; + private bool initialized; + private ushort territoryTypeId; + private bool isPvP; + private uint mapId; + private uint instance; private bool lastConditionNone = true; [ServiceManager.ServiceConstructor] @@ -59,26 +65,37 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; - var setTerritoryTypeAddr = EventFramework.Addresses.SetTerritoryTypeId.Value; - Log.Verbose($"SetupTerritoryType address {Util.DescribeAddress(setTerritoryTypeAddr)}"); - - this.setupTerritoryTypeHook = Hook.FromAddress(setTerritoryTypeAddr, this.SetupTerritoryTypeDetour); + this.handleZoneInitPacketHook = Hook.FromAddress(this.AddressResolver.HandleZoneInitPacket, this.HandleZoneInitPacketDetour); this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); + this.setCurrentInstanceHook = Hook.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); - this.framework.Update += this.FrameworkOnOnUpdateEvent; this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; - this.setupTerritoryTypeHook.Enable(); + this.handleZoneInitPacketHook.Enable(); this.uiModuleHandlePacketHook.Enable(); + this.setCurrentInstanceHook.Enable(); this.framework.RunOnTick(this.Setup); } private unsafe delegate void ProcessPacketPlayerSetupDelegate(nint a1, nint packet); + private unsafe delegate void HandleZoneInitPacketDelegate(nint a1, uint localPlayerEntityId, nint packet, byte type); + + private unsafe delegate void SetCurrentInstanceDelegate(NetworkModuleProxy* thisPtr, short instanceId); + + /// + public event Action ZoneInit; + /// public event Action? TerritoryChanged; + /// + public event Action? MapIdChanged; + + /// + public event Action? InstanceChanged; + /// public event IClientState.ClassJobChangeDelegate? ClassJobChanged; @@ -104,15 +121,65 @@ internal sealed class ClientState : IInternalDisposableService, IClientState public ClientLanguage ClientLanguage { get; } /// - public ushort TerritoryType { get; private set; } + public ushort TerritoryType + { + get => this.territoryTypeId; + private set + { + if (this.territoryTypeId != value) + { + this.territoryTypeId = value; + + if (this.initialized) + { + Log.Debug("TerritoryType changed: {0}", value); + this.TerritoryChanged?.InvokeSafely(value); + } + + var rowRef = LuminaUtils.CreateRef(value); + if (rowRef.IsValid) + { + this.IsPvP = rowRef.Value.IsPvpZone; + } + } + } + } /// - public unsafe uint MapId + public uint MapId { - get + get => this.mapId; + private set { - var agentMap = AgentMap.Instance(); - return agentMap != null ? agentMap->CurrentMapId : 0; + if (this.mapId != value) + { + this.mapId = value; + + if (this.initialized) + { + Log.Debug("MapId changed: {0}", value); + this.MapIdChanged?.InvokeSafely(value); + } + } + } + } + + /// + public uint Instance + { + get => this.instance; + private set + { + if (this.instance != value) + { + this.instance = value; + + if (this.initialized) + { + Log.Debug("Instance changed: {0}", value); + this.InstanceChanged?.InvokeSafely(value); + } + } } } @@ -133,7 +200,31 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } /// - public bool IsPvP { get; private set; } + public bool IsPvP + { + get => this.isPvP; + private set + { + if (this.isPvP != value) + { + this.isPvP = value; + + if (this.initialized) + { + if (value) + { + Log.Debug("EnterPvP"); + this.EnterPvP?.InvokeSafely(); + } + else + { + Log.Debug("LeavePvP"); + this.LeavePvP?.InvokeSafely(); + } + } + } + } + } /// public bool IsPvPExcludingDen => this.IsPvP && this.TerritoryType != 250; @@ -172,11 +263,12 @@ internal sealed class ClientState : IInternalDisposableService, IClientState /// void IInternalDisposableService.DisposeService() { - this.setupTerritoryTypeHook.Dispose(); + this.handleZoneInitPacketHook.Dispose(); this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); + this.setCurrentInstanceHook.Dispose(); - this.framework.Update -= this.FrameworkOnOnUpdateEvent; + this.framework.Update -= this.OnFrameworkUpdate; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } @@ -186,43 +278,28 @@ internal sealed class ClientState : IInternalDisposableService, IClientState this.onLogoutHook.Enable(); this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; + this.MapId = AgentMap.Instance()->CurrentMapId; + this.Instance = UIState.Instance()->PublicInstance.InstanceId; + + this.initialized = true; + + this.framework.Update += this.OnFrameworkUpdate; } - private unsafe void SetupTerritoryTypeDetour(EventFramework* eventFramework, ushort territoryType) + private void HandleZoneInitPacketDetour(nint a1, uint localPlayerEntityId, nint packet, byte type) { - this.SetTerritoryType(territoryType); - this.setupTerritoryTypeHook.Original(eventFramework, territoryType); - } + this.handleZoneInitPacketHook.Original(a1, localPlayerEntityId, packet, type); - private unsafe void SetTerritoryType(ushort territoryType) - { - if (this.TerritoryType == territoryType) - return; - - Log.Debug("TerritoryType changed: {0}", territoryType); - - this.TerritoryType = territoryType; - this.TerritoryChanged?.InvokeSafely(territoryType); - - var rowRef = LuminaUtils.CreateRef(territoryType); - if (rowRef.IsValid) + try { - var isPvP = rowRef.Value.IsPvpZone; - if (isPvP != this.IsPvP) - { - this.IsPvP = isPvP; - - if (this.IsPvP) - { - Log.Debug("EnterPvP"); - this.EnterPvP?.InvokeSafely(); - } - else - { - Log.Debug("LeavePvP"); - this.LeavePvP?.InvokeSafely(); - } - } + var eventArgs = ZoneInitEventArgs.Read(packet); + Log.Debug($"ZoneInit: {eventArgs}"); + this.ZoneInit?.InvokeSafely(eventArgs); + this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; + } + catch (Exception ex) + { + Log.Error(ex, "Exception during ZoneInit"); } } @@ -274,8 +351,16 @@ internal sealed class ClientState : IInternalDisposableService, IClientState } } - private void FrameworkOnOnUpdateEvent(IFramework framework1) + private unsafe void SetCurrentInstanceDetour(NetworkModuleProxy* thisPtr, short instanceId) { + this.setCurrentInstanceHook.Original(thisPtr, instanceId); + this.Instance = (uint)instanceId; + } + + private unsafe void OnFrameworkUpdate(IFramework framework) + { + this.MapId = AgentMap.Instance()->CurrentMapId; + var condition = Service.GetNullable(); var gameGui = Service.GetNullable(); var data = Service.GetNullable(); @@ -357,7 +442,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// internal ClientStatePluginScoped() { + this.clientStateService.ZoneInit += this.ZoneInitForward; this.clientStateService.TerritoryChanged += this.TerritoryChangedForward; + this.clientStateService.MapIdChanged += this.MapIdChangedForward; + this.clientStateService.InstanceChanged += this.InstanceChangedForward; this.clientStateService.ClassJobChanged += this.ClassJobChangedForward; this.clientStateService.LevelChanged += this.LevelChangedForward; this.clientStateService.Login += this.LoginForward; @@ -367,9 +455,18 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.clientStateService.CfPop += this.ContentFinderPopForward; } + /// + public event Action ZoneInit; + /// public event Action? TerritoryChanged; + /// + public event Action? MapIdChanged; + + /// + public event Action? InstanceChanged; + /// public event IClientState.ClassJobChangeDelegate? ClassJobChanged; @@ -400,6 +497,9 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// public uint MapId => this.clientStateService.MapId; + /// + public uint Instance => this.clientStateService.Instance; + /// public IPlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; @@ -427,7 +527,10 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat /// void IInternalDisposableService.DisposeService() { + this.clientStateService.ZoneInit -= this.ZoneInitForward; this.clientStateService.TerritoryChanged -= this.TerritoryChangedForward; + this.clientStateService.MapIdChanged -= this.MapIdChangedForward; + this.clientStateService.InstanceChanged -= this.InstanceChangedForward; this.clientStateService.ClassJobChanged -= this.ClassJobChangedForward; this.clientStateService.LevelChanged -= this.LevelChangedForward; this.clientStateService.Login -= this.LoginForward; @@ -436,7 +539,12 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.clientStateService.LeavePvP -= this.ExitPvPForward; this.clientStateService.CfPop -= this.ContentFinderPopForward; + this.ZoneInit = null; this.TerritoryChanged = null; + this.MapIdChanged = null; + this.InstanceChanged = null; + this.ClassJobChanged = null; + this.LevelChanged = null; this.Login = null; this.Logout = null; this.EnterPvP = null; @@ -444,8 +552,14 @@ internal class ClientStatePluginScoped : IInternalDisposableService, IClientStat this.CfPop = null; } + private void ZoneInitForward(ZoneInitEventArgs eventArgs) => this.ZoneInit?.Invoke(eventArgs); + private void TerritoryChangedForward(ushort territoryId) => this.TerritoryChanged?.Invoke(territoryId); + private void MapIdChangedForward(uint mapId) => this.MapIdChanged?.Invoke(mapId); + + private void InstanceChangedForward(uint instanceId) => this.InstanceChanged?.Invoke(instanceId); + private void ClassJobChangedForward(uint classJobId) => this.ClassJobChanged?.Invoke(classJobId); private void LevelChangedForward(uint classJobId, uint level) => this.LevelChanged?.Invoke(classJobId, level); diff --git a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs index 97bc5dae1..2fc859d09 100644 --- a/Dalamud/Game/ClientState/ClientStateAddressResolver.cs +++ b/Dalamud/Game/ClientState/ClientStateAddressResolver.cs @@ -10,19 +10,24 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// /// Gets the address of the keyboard state. /// - public IntPtr KeyboardState { get; private set; } + public nint KeyboardState { get; private set; } /// /// Gets the address of the keyboard state index array which translates the VK enumeration to the key state. /// - public IntPtr KeyboardStateIndexArray { get; private set; } + public nint KeyboardStateIndexArray { get; private set; } // Functions /// - /// Gets the address of the method which sets up the player. + /// Gets the address of the method that handles the ZoneInit packet. /// - public IntPtr ProcessPacketPlayerSetup { get; private set; } + public nint HandleZoneInitPacket { get; private set; } + + /// + /// Gets the address of the method that sets the current public instance. + /// + public nint SetCurrentInstance { get; private set; } /// /// Scan for and setup any configured address pointers. @@ -30,7 +35,8 @@ internal sealed class ClientStateAddressResolver : BaseAddressResolver /// The signature scanner to facilitate setup. protected override void Setup64Bit(ISigScanner sig) { - this.ProcessPacketPlayerSetup = sig.ScanText("40 53 48 83 EC 20 48 8D 0D ?? ?? ?? ?? 48 8B DA E8 ?? ?? ?? ?? 48 8B D3"); // not in cs struct + this.HandleZoneInitPacket = sig.ScanText("E8 ?? ?? ?? ?? 48 8B 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 44 0F B6 45"); + this.SetCurrentInstance = sig.ScanText("E8 ?? ?? ?? ?? 0F B6 55 ?? 48 8D 0D ?? ?? ?? ?? C0 EA"); // NetworkModuleProxy.SetCurrentInstance // These resolve to fixed offsets only, without the base address added in, so GetStaticAddressFromSig() can't be used. // lea rcx, ds:1DB9F74h[rax*4] KeyboardState diff --git a/Dalamud/Game/ClientState/ZoneInit.cs b/Dalamud/Game/ClientState/ZoneInit.cs new file mode 100644 index 000000000..5c2213c90 --- /dev/null +++ b/Dalamud/Game/ClientState/ZoneInit.cs @@ -0,0 +1,90 @@ +using System.Linq; +using System.Text; + +using Dalamud.Data; + +using Lumina.Excel.Sheets; + +namespace Dalamud.Game.ClientState; + +/// +/// Provides event data for when the game should initialize a zone. +/// +public class ZoneInitEventArgs : EventArgs +{ + /// + /// Gets the territory type of the zone being entered. + /// + public TerritoryType TerritoryType { get; private set; } + + /// + /// Gets the instance number of the zone, used when multiple copies of an area are active. + /// + public ushort Instance { get; private set; } + + /// + /// Gets the associated content finder condition for the zone, if any. + /// + public ContentFinderCondition ContentFinderCondition { get; private set; } + + /// + /// Gets the current weather in the zone upon entry. + /// + public Weather Weather { get; private set; } + + /// + /// Gets the set of active festivals in the zone. + /// + public Festival[] ActiveFestivals { get; private set; } = []; + + /// + /// Gets the phases corresponding to the active festivals. + /// + public ushort[] ActiveFestivalPhases { get; private set; } = []; + + /// + /// Reads raw zone initialization data from a network packet and constructs the event arguments. + /// + /// A pointer to the raw packet data. + /// A populated from the packet. + public static unsafe ZoneInitEventArgs Read(nint packet) + { + var dataManager = Service.Get(); + var eventArgs = new ZoneInitEventArgs(); + + var flags = *(byte*)(packet + 0x12); + + eventArgs.TerritoryType = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x02)); + eventArgs.Instance = flags >= 0 ? (ushort)0 : *(ushort*)(packet + 0x04); + eventArgs.ContentFinderCondition = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x06)); + eventArgs.Weather = dataManager.GetExcelSheet().GetRow(*(byte*)(packet + 0x10)); + + const int NumFestivals = 4; + eventArgs.ActiveFestivals = new Festival[NumFestivals]; + eventArgs.ActiveFestivalPhases = new ushort[NumFestivals]; + + // There are also 4 festival ids and phases for PlayerState at +0x3E and +0x46 respectively, + // but it's unclear why they exist as separate entries and why they would be different. + for (var i = 0; i < NumFestivals; i++) + { + eventArgs.ActiveFestivals[i] = dataManager.GetExcelSheet().GetRow(*(ushort*)(packet + 0x2E + (i * 2))); + eventArgs.ActiveFestivalPhases[i] = *(ushort*)(packet + 0x36 + (i * 2)); + } + + return eventArgs; + } + + /// + public override string ToString() + { + var sb = new StringBuilder("ZoneInitEventArgs { "); + sb.Append($"TerritoryTypeId = {this.TerritoryType.RowId}, "); + sb.Append($"Instance = {this.Instance}, "); + sb.Append($"ContentFinderCondition = {this.ContentFinderCondition.RowId}, "); + sb.Append($"Weather = {this.Weather.RowId}, "); + sb.Append($"ActiveFestivals = [{string.Join(", ", this.ActiveFestivals.Select(f => f.RowId))}], "); + sb.Append($"ActiveFestivalPhases = [{string.Join(", ", this.ActiveFestivalPhases)}]"); + sb.Append(" }"); + return sb.ToString(); + } +} diff --git a/Dalamud/Plugin/Services/IClientState.cs b/Dalamud/Plugin/Services/IClientState.cs index 60d8a17e2..0342ea77c 100644 --- a/Dalamud/Plugin/Services/IClientState.cs +++ b/Dalamud/Plugin/Services/IClientState.cs @@ -1,4 +1,5 @@ using Dalamud.Game; +using Dalamud.Game.ClientState; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; @@ -29,11 +30,26 @@ public interface IClientState /// The success/failure code. public delegate void LogoutDelegate(int type, int code); + /// + /// Event that gets fired when the game initializes a zone. + /// + public event Action ZoneInit; + /// /// Event that gets fired when the current Territory changes. /// public event Action TerritoryChanged; + /// + /// Event that gets fired when the current Map changes. + /// + public event Action MapIdChanged; + + /// + /// Event that gets fired when the current zone Instance changes. + /// + public event Action InstanceChanged; + /// /// Event that fires when a characters ClassJob changed. /// @@ -85,6 +101,11 @@ public interface IClientState /// public uint MapId { get; } + /// + /// Gets the instance number of the current zone, used when multiple copies of an area are active. + /// + public uint Instance { get; } + /// /// Gets the local player character, if one is present. ///