using System; using System.Collections.Generic; using System.Runtime.CompilerServices; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Object; using FFXIVClientStructs.FFXIV.Client.Game.UI; using LLib.Inventory; using Microsoft.Extensions.Logging; using Questionable.Controller.GameUi; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class Interact { internal sealed class Factory(Configuration configuration) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { EInteractionType interactionType = step.InteractionType; if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 30) <= 1u) ? true : false) { if ((step.InteractionType == EInteractionType.CompleteQuest && configuration.Advanced.PreventQuestCompletion) || step.Emote.HasValue || step.ChatMessage != null || step.ItemId.HasValue || !step.DataId.HasValue) { yield break; } } else if (step.InteractionType == EInteractionType.PurchaseItem) { if (!step.DataId.HasValue) { yield break; } } else if (step.InteractionType == EInteractionType.Snipe) { if (!configuration.General.AutoSnipe) { yield break; } } else if (step.InteractionType == EInteractionType.UnlockTaxiStand) { if (!step.TaxiStandId.HasValue) { yield break; } } else if (step.InteractionType != EInteractionType.Interact) { yield break; } ArgumentNullException.ThrowIfNull(step.DataId, "step.DataId"); if (sequence.Sequence == 0 && sequence.Steps.IndexOf(step) == 0) { yield return new WaitAtEnd.WaitDelay(); } uint value = step.DataId.Value; EInteractionType interactionType2 = step.InteractionType; int skipMarkerCheck; if (!step.IgnoreQuestMarker && !step.TargetTerritoryId.HasValue) { SkipConditions skipConditions = step.SkipConditions; if (skipConditions != null) { SkipStepConditions stepIf = skipConditions.StepIf; if (stepIf != null && stepIf.Never) { goto IL_024a; } } if (step.InteractionType != EInteractionType.PurchaseItem) { skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0); goto IL_024b; } } goto IL_024a; IL_024a: skipMarkerCheck = 1; goto IL_024b; IL_024b: uint? pickUpItemId = step.PickUpItemId ?? step.GCPurchase?.ItemId; byte? taxiStandId = step.TaxiStandId; SkipStepConditions? skipConditions2 = step.SkipConditions?.StepIf; List completionQuestVariablesFlags = step.CompletionQuestVariablesFlags; uint? purchaseItemId = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemId : ((uint?)null)); int purchaseItemCount = ((step.InteractionType == EInteractionType.PurchaseItem) ? step.ItemCount.GetValueOrDefault() : 0); yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, pickUpItemId, taxiStandId, skipConditions2, completionQuestVariablesFlags, null, purchaseItemId, purchaseItemCount); } } internal sealed record Task : ITask { public uint DataId { get; init; } public Quest? Quest { get; init; } public EInteractionType InteractionType { get; init; } public bool SkipMarkerCheck { get; init; } public uint? PickUpItemId { get; init; } public byte? TaxiStandId { get; init; } public SkipStepConditions? SkipConditions { get; init; } public EStatus? CompletionStatusId { get; init; } public uint? PurchaseItemId { get; init; } public int PurchaseItemCount { get; init; } public List CompletionQuestVariablesFlags { get; } public bool HasCompletionQuestVariablesFlags { get; } public Task(uint DataId, Quest? Quest, EInteractionType InteractionType, bool SkipMarkerCheck = false, uint? PickUpItemId = null, byte? TaxiStandId = null, SkipStepConditions? SkipConditions = null, List? CompletionQuestVariablesFlags = null, EStatus? CompletionStatusId = null, uint? PurchaseItemId = null, int PurchaseItemCount = 0) { this.DataId = DataId; this.Quest = Quest; this.InteractionType = InteractionType; this.SkipMarkerCheck = SkipMarkerCheck; this.PickUpItemId = PickUpItemId; this.TaxiStandId = TaxiStandId; this.SkipConditions = SkipConditions; this.CompletionStatusId = CompletionStatusId; this.PurchaseItemId = PurchaseItemId; this.PurchaseItemCount = PurchaseItemCount; this.CompletionQuestVariablesFlags = CompletionQuestVariablesFlags ?? new List(); HasCompletionQuestVariablesFlags = Quest != null && CompletionQuestVariablesFlags != null && QuestWorkUtils.HasCompletionFlags(CompletionQuestVariablesFlags); base._002Ector(); } public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return $"Interact{(HasCompletionQuestVariablesFlags ? "*" : "")}({DataId})"; } [CompilerGenerated] public void Deconstruct(out uint DataId, out Quest? Quest, out EInteractionType InteractionType, out bool SkipMarkerCheck, out uint? PickUpItemId, out byte? TaxiStandId, out SkipStepConditions? SkipConditions, out List? CompletionQuestVariablesFlags, out EStatus? CompletionStatusId, out uint? PurchaseItemId, out int PurchaseItemCount) { DataId = this.DataId; Quest = this.Quest; InteractionType = this.InteractionType; SkipMarkerCheck = this.SkipMarkerCheck; PickUpItemId = this.PickUpItemId; TaxiStandId = this.TaxiStandId; SkipConditions = this.SkipConditions; CompletionQuestVariablesFlags = this.CompletionQuestVariablesFlags; CompletionStatusId = this.CompletionStatusId; PurchaseItemId = this.PurchaseItemId; PurchaseItemCount = this.PurchaseItemCount; } } internal sealed class DoInteract(GameFunctions gameFunctions, QuestFunctions questFunctions, ShopController shopController, ICondition condition, ILogger logger) : TaskExecutor(), IConditionChangeAware, ITaskExecutor { private enum EInteractionState { None, InteractionTriggered, InteractionConfirmed } private const long GatheringNodeLoadTimeoutMs = 1000L; private bool _needsUnmount; private EInteractionState _interactionState; private long _continueAt; private int _interactionAttempts; private long _gatheringNodeMissingSince; private bool _delayedFinalCheck; public Quest? Quest => base.Task.Quest; public EInteractionType InteractionType { get; set; } protected override bool Start() { InteractionType = base.Task.InteractionType; _interactionState = EInteractionState.None; _needsUnmount = false; _delayedFinalCheck = false; _continueAt = 0L; _interactionAttempts = 0; _gatheringNodeMissingSince = 0L; IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId, null, warnIfMissing: false); if (gameObject == null) { if (InteractionType == EInteractionType.Gather) { _gatheringNodeMissingSince = Environment.TickCount64; } logger.LogDebug("Object with dataId {DataId} not found yet, waiting", base.Task.DataId); return true; } if (!gameObject.IsTargetable) { SkipStepConditions skipConditions = base.Task.SkipConditions; if (skipConditions != null && !skipConditions.Never && skipConditions.NotTargetable) { logger.LogDebug("Not interacting with {DataId} because it is not targetable (but skippable)", base.Task.DataId); return false; } } if (!gameObject.IsTargetable && condition[ConditionFlag.Mounted] && gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.GatheringPoint) { logger.LogDebug("Preparing interaction for {DataId} by unmounting", base.Task.DataId); _needsUnmount = true; gameFunctions.Unmount(); _continueAt = Environment.TickCount64 + 1000; return true; } if (gameObject.IsTargetable && HasAnyMarker(gameObject)) { TriggerInteraction(gameObject); return true; } return true; } public unsafe override ETaskResult Update() { if (Environment.TickCount64 <= _continueAt) { return ETaskResult.StillRunning; } if (_needsUnmount) { if (condition[ConditionFlag.Mounted]) { gameFunctions.Unmount(); _continueAt = Environment.TickCount64 + 1000; return ETaskResult.StillRunning; } _needsUnmount = false; } EStatus? completionStatusId = base.Task.CompletionStatusId; if (completionStatusId.HasValue) { EStatus valueOrDefault = completionStatusId.GetValueOrDefault(); if (gameFunctions.HasStatus(valueOrDefault)) { ResetProgressTimer(); return ETaskResult.TaskComplete; } } uint? pickUpItemId = base.Task.PickUpItemId; if (pickUpItemId.HasValue) { uint valueOrDefault2 = pickUpItemId.GetValueOrDefault(); if (InventoryManager.Instance()->GetInventoryItemCount(valueOrDefault2, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0) { ResetProgressTimer(); return ETaskResult.TaskComplete; } } else { pickUpItemId = base.Task.PurchaseItemId; if (pickUpItemId.HasValue) { uint valueOrDefault3 = pickUpItemId.GetValueOrDefault(); if (base.Task.PurchaseItemCount > 0) { if (((ItemHandle)valueOrDefault3).GetCount(checkEquipped: true, checkArmory: true) >= base.Task.PurchaseItemCount) { ResetProgressTimer(); return ETaskResult.TaskComplete; } if (shopController.IsOpen) { return ETaskResult.StillRunning; } goto IL_0252; } } byte? taxiStandId = base.Task.TaxiStandId; if (taxiStandId.HasValue) { byte valueOrDefault4 = taxiStandId.GetValueOrDefault(); if (UIState.Instance()->IsChocoboTaxiStandUnlocked((uint)(valueOrDefault4 + 1179648))) { ResetProgressTimer(); return ETaskResult.TaskComplete; } } else { if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering]) { ResetProgressTimer(); return ETaskResult.TaskComplete; } if (Quest != null && base.Task.HasCompletionQuestVariablesFlags) { QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(Quest.Id); if (questProgressInfo != null && QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo)) { ResetProgressTimer(); return ETaskResult.TaskComplete; } } else if (base.ProgressContext != null) { if (base.ProgressContext.WasInterrupted()) { logger.LogDebug("Interaction with {DataId} was interrupted", base.Task.DataId); return ETaskResult.StillRunning; } if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed) { ResetProgressTimer(); if (_delayedFinalCheck) { return ETaskResult.TaskComplete; } _continueAt = Environment.TickCount64 + 200; _delayedFinalCheck = true; return ETaskResult.StillRunning; } } } } goto IL_0252; IL_0252: IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId, null, warnIfMissing: false); if (gameObject == null || !gameObject.IsTargetable) { if (InteractionType == EInteractionType.Gather) { if (_gatheringNodeMissingSince == 0L) { _gatheringNodeMissingSince = Environment.TickCount64; } if (Environment.TickCount64 - _gatheringNodeMissingSince >= 1000) { logger.LogDebug("Gathering node {DataId} is not active, selecting another node", base.Task.DataId); return ETaskResult.SkipRemainingTasksForStep; } } return ETaskResult.StillRunning; } _gatheringNodeMissingSince = 0L; if (!HasAnyMarker(gameObject)) { return ETaskResult.StillRunning; } _interactionAttempts++; if (_interactionAttempts > 10) { logger.LogWarning("Interaction with {DataId} failed after {Attempts} attempts, retrying step", base.Task.DataId, _interactionAttempts); return ETaskResult.RetryStep; } TriggerInteraction(gameObject); ResetProgressTimer(); return ETaskResult.StillRunning; } private void TriggerInteraction(IGameObject gameObject) { base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(delegate { _interactionState = (gameFunctions.InteractWith(gameObject) ? EInteractionState.InteractionTriggered : EInteractionState.None); return _interactionState != EInteractionState.None; }); _continueAt = Environment.TickCount64 + 500; } private unsafe bool HasAnyMarker(IGameObject gameObject) { if (base.Task.SkipMarkerCheck || gameObject.ObjectKind != Dalamud.Game.ClientState.Objects.Enums.ObjectKind.EventNpc) { return true; } GameObject* address = (GameObject*)gameObject.Address; return address->NamePlateIconId != 0; } public void OnConditionChange(ConditionFlag flag, bool value) { if (base.ProgressContext == null || (!base.ProgressContext.WasInterrupted() && !base.ProgressContext.WasSuccessful())) { logger.LogDebug("Condition change: {Flag} = {Value}", flag, value); bool flag2 = _interactionState == EInteractionState.InteractionTriggered; if (flag2) { bool flag3 = (uint)(flag - 31) <= 1u; flag2 = flag3; } if (flag2 && value) { logger.LogDebug("Interaction was most likely triggered"); _interactionState = EInteractionState.InteractionConfirmed; } } } public override bool ShouldInterruptOnDamage() { return true; } } }