using System.Linq; using Dalamud.Data; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.Gui; using Dalamud.Game.Network.Internal; using Dalamud.Game.Player; using Dalamud.Hooking; using Dalamud.IoC; using Dalamud.IoC.Internal; using Dalamud.Logging.Internal; using Dalamud.Plugin.Services; using Dalamud.Utility; using FFXIVClientStructs.FFXIV.Application.Network; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Network; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using Lumina.Excel.Sheets; using Action = System.Action; using CSUIState = FFXIVClientStructs.FFXIV.Client.Game.UI.UIState; namespace Dalamud.Game.ClientState; /// /// This class represents the state of the game client at the time of access. /// [ServiceManager.EarlyLoadedService] internal sealed class ClientState : IInternalDisposableService, IClientState { private static readonly ModuleLog Log = ModuleLog.Create(); private readonly GameLifecycle lifecycle; private readonly ClientStateAddressResolver address; private readonly Hook uiModuleHandlePacketHook; private readonly Hook setCurrentInstanceHook; [ServiceManager.ServiceDependency] private readonly Framework framework = Service.Get(); [ServiceManager.ServiceDependency] private readonly NetworkHandlers networkHandlers = Service.Get(); [ServiceManager.ServiceDependency] private readonly PlayerState playerState = Service.Get(); [ServiceManager.ServiceDependency] private readonly ObjectTable objectTable = 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] private unsafe ClientState(TargetSigScanner sigScanner, Dalamud dalamud, GameLifecycle lifecycle) { this.lifecycle = lifecycle; this.address = new ClientStateAddressResolver(); this.address.Setup(sigScanner); Log.Verbose("===== C L I E N T S T A T E ====="); this.ClientLanguage = (ClientLanguage)dalamud.StartInfo.Language; this.uiModuleHandlePacketHook = Hook.FromAddress((nint)UIModule.StaticVirtualTablePointer->HandlePacket, this.UIModuleHandlePacketDetour); this.setCurrentInstanceHook = Hook.FromAddress(this.AddressResolver.SetCurrentInstance, this.SetCurrentInstanceDetour); this.networkHandlers.CfPop += this.NetworkHandlersOnCfPop; 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; /// public event IClientState.LevelChangeDelegate? LevelChanged; /// public event Action? Login; /// public event IClientState.LogoutDelegate? Logout; /// public event Action? EnterPvP; /// public event Action? LeavePvP; /// public event Action? CfPop; /// public ClientLanguage ClientLanguage { get; } /// 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 uint MapId { get => this.mapId; private set { 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); } } } } /// public IPlayerCharacter? LocalPlayer => this.objectTable.LocalPlayer; /// public unsafe ulong LocalContentId => this.playerState.ContentId; /// public unsafe bool IsLoggedIn { get { var agentLobby = AgentLobby.Instance(); return agentLobby != null && agentLobby->IsLoggedIn; } } /// 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; /// public bool IsGPosing => GameMain.IsInGPose(); /// /// Gets client state address resolver. /// internal ClientStateAddressResolver AddressResolver => this.address; /// public bool IsClientIdle(out ConditionFlag blockingFlag) { blockingFlag = 0; if (this.objectTable.LocalPlayer is null) return true; var condition = Service.GetNullable(); var blockingConditions = condition.AsReadOnlySet().Except([ ConditionFlag.NormalConditions, ConditionFlag.Jumping, ConditionFlag.Mounted, ConditionFlag.UsingFashionAccessory, ConditionFlag.OnFreeTrial]); blockingFlag = blockingConditions.FirstOrDefault(); return blockingFlag == 0; } /// public bool IsClientIdle() => this.IsClientIdle(out _); /// /// Dispose of managed and unmanaged resources. /// void IInternalDisposableService.DisposeService() { this.uiModuleHandlePacketHook.Dispose(); this.onLogoutHook.Dispose(); this.setCurrentInstanceHook.Dispose(); this.framework.Update -= this.OnFrameworkUpdate; this.networkHandlers.CfPop -= this.NetworkHandlersOnCfPop; } private unsafe void Setup() { this.onLogoutHook = Hook.FromAddress((nint)AgentLobby.Instance()->LogoutCallbackInterface.VirtualTable->OnLogout, this.OnLogoutDetour); this.onLogoutHook.Enable(); this.TerritoryType = (ushort)GameMain.Instance()->CurrentTerritoryTypeId; this.MapId = AgentMap.Instance()->CurrentMapId; this.Instance = CSUIState.Instance()->PublicInstance.InstanceId; this.initialized = true; this.framework.Update += this.OnFrameworkUpdate; } 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 Delegate.EnumerateInvocationList(this.ClassJobChanged)) { 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 Delegate.EnumerateInvocationList(this.LevelChanged)) { try { action(classJobId, level); } catch (Exception ex) { Log.Error(ex, "Exception during raise of {handler}", action.Method); } } break; } case (UIModulePacketType)5: // TODO: Use UIModulePacketType.InitZone when available { var eventArgs = ZoneInitEventArgs.Read((nint)packet); Log.Debug($"ZoneInit: {eventArgs}"); this.ZoneInit?.InvokeSafely(eventArgs); this.TerritoryType = (ushort)eventArgs.TerritoryType.RowId; break; } } } 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(); if (condition == null || gameGui == null || data == null) return; if (condition.Any() && this.lastConditionNone && this.objectTable.LocalPlayer != null) { Log.Debug("Is login"); this.lastConditionNone = false; this.Login?.InvokeSafely(); gameGui.ResetUiHideState(); this.lifecycle.ResetLogout(); } } private unsafe void OnLogoutDetour(LogoutCallbackInterface* thisPtr, LogoutCallbackInterface.LogoutParams* logoutParams) { var gameGui = Service.GetNullable(); if (logoutParams != null) { try { var type = logoutParams->Type; var code = logoutParams->Code; Log.Debug("Logout: Type {type}, Code {code}", type, code); foreach (var action in Delegate.EnumerateInvocationList(this.Logout)) { try { action(type, code); } catch (Exception ex) { Log.Error(ex, "Exception during raise of {handler}", action.Method); } } gameGui?.ResetUiHideState(); this.lastConditionNone = true; // unblock login flag this.lifecycle.SetLogout(); } catch (Exception ex) { Log.Error(ex, "Exception during OnLogoutDetour"); } } this.onLogoutHook.Original(thisPtr, logoutParams); } private void NetworkHandlersOnCfPop(ContentFinderCondition e) { this.CfPop?.InvokeSafely(e); } } /// /// Plugin-scoped version of a GameConfig service. /// [PluginInterface] [ServiceManager.ScopedService] #pragma warning disable SA1015 [ResolveVia] #pragma warning restore SA1015 internal class ClientStatePluginScoped : IInternalDisposableService, IClientState { [ServiceManager.ServiceDependency] private readonly ClientState clientStateService = Service.Get(); /// /// Initializes a new instance of the class. /// 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; this.clientStateService.Logout += this.LogoutForward; this.clientStateService.EnterPvP += this.EnterPvPForward; this.clientStateService.LeavePvP += this.ExitPvPForward; 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; /// public event IClientState.LevelChangeDelegate? LevelChanged; /// public event Action? Login; /// public event IClientState.LogoutDelegate? Logout; /// public event Action? EnterPvP; /// public event Action? LeavePvP; /// public event Action? CfPop; /// public ClientLanguage ClientLanguage => this.clientStateService.ClientLanguage; /// public ushort TerritoryType => this.clientStateService.TerritoryType; /// public uint MapId => this.clientStateService.MapId; /// public uint Instance => this.clientStateService.Instance; /// public IPlayerCharacter? LocalPlayer => this.clientStateService.LocalPlayer; /// public ulong LocalContentId => this.clientStateService.LocalContentId; /// public bool IsLoggedIn => this.clientStateService.IsLoggedIn; /// public bool IsPvP => this.clientStateService.IsPvP; /// public bool IsPvPExcludingDen => this.clientStateService.IsPvPExcludingDen; /// public bool IsGPosing => this.clientStateService.IsGPosing; /// public bool IsClientIdle(out ConditionFlag blockingFlag) => this.clientStateService.IsClientIdle(out blockingFlag); /// public bool IsClientIdle() => this.clientStateService.IsClientIdle(); /// 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; this.clientStateService.Logout -= this.LogoutForward; this.clientStateService.EnterPvP -= this.EnterPvPForward; 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; this.LeavePvP = null; 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); private void LoginForward() => this.Login?.Invoke(); private void LogoutForward(int type, int code) => this.Logout?.Invoke(type, code); private void EnterPvPForward() => this.EnterPvP?.Invoke(); private void ExitPvPForward() => this.LeavePvP?.Invoke(); private void ContentFinderPopForward(ContentFinderCondition cfc) => this.CfPop?.Invoke(cfc); }