using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Numerics; using System.Runtime.InteropServices; using Dalamud.Game; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Character; using FFXIVClientStructs.FFXIV.Client.Game.Control; using FFXIVClientStructs.FFXIV.Client.Game.Fate; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Client.System.String; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; using Lumina.Excel.Sheets; using Microsoft.Extensions.Logging; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Functions; internal sealed class GameFunctions { private delegate void AbandonDutyDelegate(bool a1); private static class Signatures { internal const string AbandonDuty = "E8 ?? ?? ?? ?? 41 B2 01 EB 39"; } private readonly QuestFunctions _questFunctions; private readonly IDataManager _dataManager; private readonly IObjectTable _objectTable; private readonly ITargetManager _targetManager; private readonly ICondition _condition; private readonly IClientState _clientState; private readonly IGameGui _gameGui; private readonly Configuration _configuration; private readonly ILogger _logger; private readonly AbandonDutyDelegate _abandonDuty; private readonly ReadOnlyDictionary _territoryToAetherCurrentCompFlgSet; private readonly ReadOnlyDictionary _contentFinderConditionToContentId; public GameFunctions(QuestFunctions questFunctions, IDataManager dataManager, IObjectTable objectTable, ITargetManager targetManager, ICondition condition, IClientState clientState, IGameGui gameGui, Configuration configuration, ISigScanner sigScanner, ILogger logger) { _questFunctions = questFunctions; _dataManager = dataManager; _objectTable = objectTable; _targetManager = targetManager; _condition = condition; _clientState = clientState; _gameGui = gameGui; _configuration = configuration; _logger = logger; _abandonDuty = Marshal.GetDelegateForFunctionPointer(sigScanner.ScanText("E8 ?? ?? ?? ?? 41 B2 01 EB 39")); _territoryToAetherCurrentCompFlgSet = (from x in dataManager.GetExcelSheet() where x.RowId != 0 where x.AetherCurrentCompFlgSet.RowId != 0 select x).ToDictionary((TerritoryType x) => (ushort)x.RowId, (TerritoryType x) => x.AetherCurrentCompFlgSet.RowId).AsReadOnly(); _contentFinderConditionToContentId = (from x in dataManager.GetExcelSheet() where x.RowId != 0 && x.Content.RowId != 0 select x).ToDictionary((ContentFinderCondition x) => x.RowId, (ContentFinderCondition x) => x.Content.RowId).AsReadOnly(); } public unsafe bool IsFlyingUnlocked(ushort territoryId) { if (_configuration.Advanced.NeverFly) { return false; } if (_questFunctions.IsQuestAccepted(new QuestId(3304)) && _condition[ConditionFlag.Mounted] && GetMountId() == 198) { return true; } PlayerState* ptr = PlayerState.Instance(); if (ptr != null && _territoryToAetherCurrentCompFlgSet.TryGetValue(territoryId, out var value)) { return ptr->IsAetherCurrentZoneComplete(value); } return false; } public unsafe ushort? GetMountId() { BattleChara* ptr = (BattleChara*)(_clientState.LocalPlayer?.Address ?? 0); if (ptr != null && ptr->Mount.MountId != 0) { return ptr->Mount.MountId; } return null; } public bool IsFlyingUnlockedInCurrentZone() { return IsFlyingUnlocked(_clientState.TerritoryType); } public unsafe bool IsAetherCurrentUnlocked(uint aetherCurrentId) { PlayerState* ptr = PlayerState.Instance(); if (ptr != null) { return ptr->IsAetherCurrentUnlocked(aetherCurrentId); } return false; } public IGameObject? FindObjectByDataId(uint dataId, Dalamud.Game.ClientState.Objects.Enums.ObjectKind? kind = null) { foreach (IGameObject item in _objectTable) { Dalamud.Game.ClientState.Objects.Enums.ObjectKind objectKind = item.ObjectKind; bool flag = ((objectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Player || objectKind - 8 <= Dalamud.Game.ClientState.Objects.Enums.ObjectKind.BattleNpc || objectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.Housing) ? true : false); if (!flag && (item == null || item.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint || item.IsTargetable) && item.DataId == dataId && (!kind.HasValue || kind.Value == item.ObjectKind)) { return item; } } _logger.LogWarning("Could not find GameObject with dataId {DataId}", dataId); return null; } public bool InteractWith(uint dataId, Dalamud.Game.ClientState.Objects.Enums.ObjectKind? kind = null) { IGameObject gameObject = FindObjectByDataId(dataId, kind); if (gameObject != null) { return InteractWith(gameObject); } _logger.LogDebug("Game object is null"); return false; } public unsafe bool InteractWith(IGameObject gameObject) { _logger.LogInformation("Setting target with {DataId} to {ObjectId}", gameObject.DataId, gameObject.EntityId); _targetManager.Target = null; _targetManager.Target = gameObject; if (gameObject.ObjectKind == Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint) { TargetSystem.Instance()->OpenObjectInteraction((GameObject*)gameObject.Address); _logger.LogInformation("Interact result: (none) for GatheringPoint"); return true; } long num = (long)TargetSystem.Instance()->InteractWithObject((GameObject*)gameObject.Address, checkLineOfSight: false); _logger.LogInformation("Interact result: {Result}", num); if (num != 7) { return num > 0; } return false; } public unsafe bool UseItem(uint itemId) { long num = AgentInventoryContext.Instance()->UseItem(itemId, InventoryType.Invalid, 0u, 0); _logger.LogInformation("UseItem result: {Result}", num); return num == 0; } public unsafe bool UseItem(uint dataId, uint itemId) { IGameObject gameObject = FindObjectByDataId(dataId); if (gameObject != null) { _targetManager.Target = gameObject; long num = AgentInventoryContext.Instance()->UseItem(itemId, InventoryType.Invalid, 0u, 0); _logger.LogInformation("UseItem result on {DataId}: {Result}", dataId, num); if ((ulong)num <= 1uL) { return true; } return false; } return false; } public unsafe bool UseItemOnGround(uint dataId, uint itemId) { IGameObject gameObject = FindObjectByDataId(dataId); if (gameObject != null) { Vector3 position = gameObject.Position; return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, 3758096384uL, &position, 0u, 0); } return false; } public unsafe bool UseItemOnPosition(Vector3 position, uint itemId) { return ActionManager.Instance()->UseActionLocation(ActionType.KeyItem, itemId, 3758096384uL, &position, 0u, 0); } public unsafe bool UseAction(EAction action) { uint num = (uint)(action & (EAction)65535); ActionType actionType = (((action & (EAction)65536) != (EAction)65536) ? ActionType.Action : ActionType.GeneralAction); if (actionType == ActionType.Action) { num = ActionManager.Instance()->GetAdjustedActionId(num); } if (ActionManager.Instance()->GetActionStatus(actionType, num, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { bool flag = ActionManager.Instance()->UseAction(actionType, num, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null); _logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) result: {Result}", action, num, flag); return flag; } return false; } public unsafe bool UseAction(IGameObject gameObject, EAction action, bool checkCanUse = true) { uint actionId = (uint)(action & (EAction)65535); ActionType actionType = (((action & (EAction)65536) != (EAction)65536) ? ActionType.Action : ActionType.GeneralAction); if (actionType == ActionType.GeneralAction) { _logger.LogWarning("Can not use general action {Action} on target {Target}", action, gameObject); return false; } actionId = ActionManager.Instance()->GetAdjustedActionId(actionId); if (checkCanUse && !ActionManager.CanUseActionOnTarget(actionId, (GameObject*)gameObject.Address)) { _logger.LogWarning("Can not use action {Action} (adjusted: {AdjustedActionId}) on target {Target}", action, actionId, gameObject); return false; } Lumina.Excel.Sheets.Action row = _dataManager.GetExcelSheet().GetRow(actionId); _targetManager.Target = gameObject; if (ActionManager.Instance()->GetActionStatus(actionType, actionId, gameObject.GameObjectId, checkRecastActive: true, checkCastingActive: true, null) == 0) { bool flag; if (row.TargetArea) { Vector3 position = gameObject.Position; flag = ActionManager.Instance()->UseActionLocation(actionType, actionId, 3758096384uL, &position, 0u, 0); _logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) on target area {Target} result: {Result}", action, actionId, gameObject, flag); } else { flag = ActionManager.Instance()->UseAction(actionType, actionId, gameObject.GameObjectId, 0u, ActionManager.UseActionMode.None, 0u, null); _logger.LogInformation("UseAction {Action} (adjusted: {AdjustedActionId}) on target {Target} result: {Result}", action, actionId, gameObject, flag); } return flag; } return false; } public bool IsObjectAtPosition(uint dataId, Vector3 position, float distance) { IGameObject gameObject = FindObjectByDataId(dataId); if (gameObject != null) { return (gameObject.Position - position).Length() < distance; } return false; } public unsafe bool HasStatusPreventingMount() { if (_condition[ConditionFlag.Swimming] && !IsFlyingUnlockedInCurrentZone()) { return true; } PlayerState* ptr = PlayerState.Instance(); if (ptr != null && !ptr->IsMountUnlocked(1u)) { return true; } IPlayerCharacter localPlayer = _clientState.LocalPlayer; if (localPlayer == null) { return false; } BattleChara* address = (BattleChara*)localPlayer.Address; StatusManager* statusManager = address->GetStatusManager(); if (statusManager->HasStatus(1151u) || statusManager->HasStatus(1945u)) { return true; } return HasCharacterStatusPreventingMountOrSprint(); } public bool HasStatusPreventingSprint() { return HasCharacterStatusPreventingMountOrSprint(); } private unsafe bool HasCharacterStatusPreventingMountOrSprint() { IPlayerCharacter localPlayer = _clientState.LocalPlayer; if (localPlayer == null) { return false; } BattleChara* address = (BattleChara*)localPlayer.Address; StatusManager* statusManager = address->GetStatusManager(); if (!statusManager->HasStatus(565u) && !statusManager->HasStatus(404u) && !statusManager->HasStatus(416u) && !statusManager->HasStatus(2729u)) { return statusManager->HasStatus(2730u); } return true; } public unsafe bool HasStatus(EStatus statusId) { IPlayerCharacter localPlayer = _clientState.LocalPlayer; if (localPlayer == null) { return false; } BattleChara* address = (BattleChara*)localPlayer.Address; return address->GetStatusManager()->HasStatus((uint)statusId); } public static bool RemoveStatus(EStatus statusId) { return StatusManager.ExecuteStatusOff((uint)statusId); } public unsafe bool Mount() { if (_condition[ConditionFlag.Mounted]) { return true; } PlayerState* ptr = PlayerState.Instance(); if (ptr != null && _configuration.General.MountId != 0 && ptr->IsMountUnlocked(_configuration.General.MountId)) { if (ActionManager.Instance()->GetActionStatus(ActionType.Mount, _configuration.General.MountId, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Attempting to use preferred mount..."); if (ActionManager.Instance()->UseAction(ActionType.Mount, _configuration.General.MountId, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null)) { _logger.LogInformation("Using preferred mount"); return true; } return false; } } else if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 9u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Attempting to use mount roulette..."); if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 9u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null)) { _logger.LogInformation("Using mount roulette"); return true; } return false; } return false; } public unsafe bool Unmount() { if (!_condition[ConditionFlag.Mounted]) { return true; } if (ActionManager.Instance()->GetActionStatus(ActionType.GeneralAction, 23u, 3758096384uL, checkRecastActive: true, checkCastingActive: true, null) == 0) { _logger.LogDebug("Attempting to unmount..."); if (ActionManager.Instance()->UseAction(ActionType.GeneralAction, 23u, 3758096384uL, 0u, ActionManager.UseActionMode.None, 0u, null)) { _logger.LogInformation("Unmounted"); return true; } return false; } _logger.LogWarning("Can't unmount right now?"); return false; } public unsafe void OpenDutyFinder(uint contentFinderConditionId) { if (_contentFinderConditionToContentId.TryGetValue(contentFinderConditionId, out var value)) { if (UIState.IsInstanceContentUnlocked(value)) { AgentContentsFinder.Instance()->OpenRegularDuty(contentFinderConditionId); return; } _logger.LogError("Trying to access a locked duty (cf: {ContentFinderId}, content: {ContentId})", contentFinderConditionId, value); } else { _logger.LogError("Could not find content for content finder condition (cf: {ContentFinderId})", contentFinderConditionId); } } public static bool GameStringEquals(string? a, string? b) { if (a == null) { return b == null; } if (b == null) { return false; } return a.ReplaceLineEndings().Replace('–', '-') == b.ReplaceLineEndings().Replace('–', '-'); } public unsafe bool IsOccupied() { if (!_clientState.IsLoggedIn || _clientState.LocalPlayer == null) { return true; } if (IsLoadingScreenVisible()) { return true; } if (_condition[ConditionFlag.Crafting]) { if (!AgentRecipeNote.Instance()->IsAgentActive()) { return true; } if (!_condition[ConditionFlag.PreparingToCraft]) { return true; } } if (_condition[ConditionFlag.Unconscious] && _condition[ConditionFlag.SufferingStatusAffliction63] && _clientState.TerritoryType == 1052) { return false; } if (!_condition[ConditionFlag.Occupied] && !_condition[ConditionFlag.Occupied30] && !_condition[ConditionFlag.Occupied33] && !_condition[ConditionFlag.Occupied38] && !_condition[ConditionFlag.Occupied39] && !_condition[ConditionFlag.OccupiedInEvent] && !_condition[ConditionFlag.OccupiedInQuestEvent] && !_condition[ConditionFlag.OccupiedInCutSceneEvent] && !_condition[ConditionFlag.Casting] && !_condition[ConditionFlag.MountOrOrnamentTransition] && !_condition[ConditionFlag.BetweenAreas] && !_condition[ConditionFlag.BetweenAreas51] && !_condition[ConditionFlag.Jumping61] && !_condition[ConditionFlag.ExecutingGatheringAction]) { return _condition[ConditionFlag.Jumping]; } return true; } public unsafe bool IsOccupiedWithCustomDeliveryNpc(Questionable.Model.Quest? currentQuest) { if (currentQuest == null || !(currentQuest.Info is SatisfactionSupplyInfo)) { return false; } if (_targetManager.Target == null || _targetManager.Target.DataId != currentQuest.Info.IssuerDataId) { return false; } if (!AgentSatisfactionSupply.Instance()->IsAgentActive()) { return false; } HashSet hashSet = _condition.AsReadOnlySet().ToHashSet(); hashSet.Remove(ConditionFlag.InDutyQueue); if (hashSet.Count == 2 && hashSet.Contains(ConditionFlag.NormalConditions)) { return hashSet.Contains(ConditionFlag.OccupiedInQuestEvent); } return false; } public unsafe bool IsLoadingScreenVisible() { if (_gameGui.TryGetAddonByName("FadeMiddle", out var addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible) { return true; } if (_gameGui.TryGetAddonByName("FadeBack", out addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible) { return true; } if (_gameGui.TryGetAddonByName("NowLoading", out addonPtr) && LAddon.IsAddonReady(addonPtr) && addonPtr->IsVisible) { return true; } return false; } public unsafe int GetFreeInventorySlots() { InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return 0; } int num = 0; for (InventoryType inventoryType = InventoryType.Inventory1; inventoryType <= InventoryType.Inventory4; inventoryType++) { InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(inventoryType); if (inventoryContainer == null) { continue; } for (int i = 0; i < inventoryContainer->Size; i++) { InventoryItem* inventorySlot = inventoryContainer->GetInventorySlot(i); if (inventorySlot == null || inventorySlot->ItemId == 0) { num++; } } } return num; } public void AbandonDuty() { _abandonDuty(a1: false); } public unsafe IReadOnlyList GetUnlockLinks() { UIState* ptr = UIState.Instance(); if (ptr == null) { _logger.LogError("Could not query unlock links"); return Array.Empty(); } List list = new List(); for (uint num = 0u; num < ptr->UnlockLinkBitmask.Length * 8; num++) { if (ptr->IsUnlockLinkUnlocked(num)) { list.Add(num); } } _logger.LogInformation("Unlocked unlock links: {UnlockedUnlockLinks}", string.Join(", ", list)); return list; } public unsafe bool SyncToFate(uint fateId) { IPlayerCharacter localPlayer = _clientState.LocalPlayer; if (localPlayer == null) { _logger.LogWarning("Cannot sync to FATE: LocalPlayer is null"); return false; } FateManager* ptr = FateManager.Instance(); if (ptr == null || ptr->CurrentFate == null) { return false; } FateContext* currentFate = ptr->CurrentFate; byte maxLevel = currentFate->MaxLevel; if (localPlayer.Level <= maxLevel) { return true; } try { _logger.LogInformation("Syncing to FATE {FateId} (max level {MaxLevel})", currentFate->FateId, maxLevel); ExecuteCommand("/lsync"); return true; } catch (Exception exception) { _logger.LogError(exception, "Failed to sync to FATE {FateId}", fateId); return false; } } public unsafe ushort GetCurrentFateId() { FateManager* ptr = FateManager.Instance(); if (ptr == null || ptr->CurrentFate == null) { return 0; } return ptr->CurrentFate->FateId; } private unsafe void ExecuteCommand(string command) { try { UIModule* uIModule = Framework.Instance()->GetUIModule(); if (uIModule == null) { _logger.LogError("UIModule is null, cannot execute command: {Command}", command); return; } Utf8String utf8String = new Utf8String(command); try { uIModule->ProcessChatBoxEntry(&utf8String, (nint)utf8String.StringLength); _logger.LogDebug("Executed chat command: {Command}", command); } finally { ((IDisposable)utf8String/*cast due to .constrained prefix*/).Dispose(); } } catch (Exception exception) { _logger.LogError(exception, "Failed to execute command: {Command}", command); } } }