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 Microsoft.Extensions.Logging; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class Interact { internal sealed class Factory(AutomatonIpc automatonIpc, Configuration configuration) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { EInteractionType interactionType = step.InteractionType; if ((interactionType == EInteractionType.SinglePlayerDuty || (uint)(interactionType - 28) <= 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 (!automatonIpc.IsAutoSnipeEnabled) { 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.TargetTerritoryId.HasValue && !(quest.Id is SatisfactionSupplyNpcId)) { SkipConditions skipConditions = step.SkipConditions; if (skipConditions != null) { SkipStepConditions stepIf = skipConditions.StepIf; if (stepIf != null && stepIf.Never) { goto IL_0247; } } if (step.InteractionType != EInteractionType.PurchaseItem) { skipMarkerCheck = ((step.DataId == 1052475) ? 1 : 0); goto IL_0248; } } goto IL_0247; IL_0247: skipMarkerCheck = 1; goto IL_0248; IL_0248: yield return new Task(value, quest, interactionType2, (byte)skipMarkerCheck != 0, step.PickUpItemId, step.TaxiStandId, step.SkipConditions?.StepIf, step.CompletionQuestVariablesFlags); } } 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 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) { this.DataId = DataId; this.Quest = Quest; this.InteractionType = InteractionType; this.SkipMarkerCheck = SkipMarkerCheck; this.PickUpItemId = PickUpItemId; this.TaxiStandId = TaxiStandId; this.SkipConditions = SkipConditions; 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) { DataId = this.DataId; Quest = this.Quest; InteractionType = this.InteractionType; SkipMarkerCheck = this.SkipMarkerCheck; PickUpItemId = this.PickUpItemId; TaxiStandId = this.TaxiStandId; SkipConditions = this.SkipConditions; CompletionQuestVariablesFlags = this.CompletionQuestVariablesFlags; } } internal sealed class DoInteract(GameFunctions gameFunctions, QuestFunctions questFunctions, ICondition condition, ILogger logger) : TaskExecutor(), IConditionChangeAware, ITaskExecutor { private enum EInteractionState { None, InteractionTriggered, InteractionConfirmed } private bool _needsUnmount; private EInteractionState _interactionState; private DateTime _continueAt = DateTime.MinValue; private bool delayedFinalCheck; public Quest? Quest => base.Task.Quest; public EInteractionType InteractionType { get; set; } protected override bool Start() { InteractionType = base.Task.InteractionType; IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId); if (gameObject == null) { logger.LogWarning("No game object with dataId {DataId}", base.Task.DataId); return false; } if (!gameObject.IsTargetable) { SkipStepConditions skipConditions = base.Task.SkipConditions; if (skipConditions != null && !skipConditions.Never && skipConditions.NotTargetable) { logger.LogInformation("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.LogInformation("Preparing interaction for {DataId} by unmounting", base.Task.DataId); _needsUnmount = true; gameFunctions.Unmount(); _continueAt = DateTime.Now.AddSeconds(1.0); return true; } if (gameObject.IsTargetable && HasAnyMarker(gameObject)) { TriggerInteraction(gameObject); return true; } return true; } public unsafe override ETaskResult Update() { if (DateTime.Now <= _continueAt) { return ETaskResult.StillRunning; } if (_needsUnmount) { if (condition[ConditionFlag.Mounted]) { gameFunctions.Unmount(); _continueAt = DateTime.Now.AddSeconds(1.0); return ETaskResult.StillRunning; } _needsUnmount = false; } uint? pickUpItemId = base.Task.PickUpItemId; if (pickUpItemId.HasValue) { uint valueOrDefault = pickUpItemId.GetValueOrDefault(); if (InventoryManager.Instance()->GetInventoryItemCount(valueOrDefault, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0) { return ETaskResult.TaskComplete; } } else { byte? taxiStandId = base.Task.TaxiStandId; if (taxiStandId.HasValue) { byte valueOrDefault2 = taxiStandId.GetValueOrDefault(); if (UIState.Instance()->IsChocoboTaxiStandUnlocked(valueOrDefault2)) { return ETaskResult.TaskComplete; } } else { if (InteractionType == EInteractionType.Gather && condition[ConditionFlag.Gathering]) { return ETaskResult.TaskComplete; } if (Quest != null && base.Task.HasCompletionQuestVariablesFlags) { QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(Quest.Id); if (questProgressInfo == null || !QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo)) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } if (base.ProgressContext != null) { if (base.ProgressContext.WasInterrupted()) { return ETaskResult.StillRunning; } if (base.ProgressContext.WasSuccessful() || _interactionState == EInteractionState.InteractionConfirmed) { if (delayedFinalCheck) { return ETaskResult.TaskComplete; } _continueAt = DateTime.Now.AddSeconds(0.2); delayedFinalCheck = true; return ETaskResult.StillRunning; } } } } IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId); if (gameObject == null || !gameObject.IsTargetable || !HasAnyMarker(gameObject)) { return ETaskResult.StillRunning; } TriggerInteraction(gameObject); return ETaskResult.StillRunning; } private void TriggerInteraction(IGameObject gameObject) { base.ProgressContext = InteractionProgressContext.FromActionUseOrDefault(delegate { if (gameFunctions.InteractWith(gameObject)) { _interactionState = EInteractionState.InteractionTriggered; } else { _interactionState = EInteractionState.None; } return _interactionState != EInteractionState.None; }); _continueAt = DateTime.Now.AddSeconds(0.5); } 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.LogInformation("Interaction was most likely triggered"); _interactionState = EInteractionState.InteractionConfirmed; } } } public override bool ShouldInterruptOnDamage() { return true; } } }