using System; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LLib; using LLib.Shop; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.Functions; namespace Questionable.Controller.CustomDelivery; internal sealed class SatisfactionSupplyTurnInExecutor(GameFunctions gameFunctions, IGameGui gameGui, ICondition condition, ILogger logger) : TaskExecutor() { private enum EPhase { InteractWithNpc, WaitForAddon, SelectSlot, ConfirmTrade, WaitForCutsceneStart, WaitForCutsceneEnd, CheckCompletion, Closing } private const long PhaseTimeoutMs = 15000L; private const long CutsceneTimeoutMs = 60000L; private Throttle _throttle; private EPhase _phase; private int _remaining; private int _lastUsedDeliveries; private long _phaseStartMs; protected override bool Start() { _phase = EPhase.InteractWithNpc; _remaining = base.Task.DeliveryCount; _lastUsedDeliveries = SatisfactionSupplyActions.GetNpcState(base.Task.NpcIndex).UsedDeliveries; _throttle.Clear(); _phaseStartMs = Environment.TickCount64; logger.LogDebug("Starting satisfaction supply turn-in: npc={NpcIndex}, slot={Slot}, count={Count}", base.Task.NpcIndex, base.Task.Slot, base.Task.DeliveryCount); return true; } public override ETaskResult Update() { long num = Environment.TickCount64 - _phaseStartMs; if ((_phase != EPhase.WaitForCutsceneEnd) ? (num > 15000) : (num > 60000)) { logger.LogWarning("Turn-in timed out in phase {Phase} after {Elapsed}ms, retrying", _phase, num); TryDismissResultAddon(); SatisfactionSupplyActions.TryCloseSatisfactionSupply(gameGui); return ETaskResult.RetryStep; } switch (_phase) { case EPhase.InteractWithNpc: if (_throttle.TryReset(0.5) && base.Task.NpcDataId != 0 && gameFunctions.InteractWith(base.Task.NpcDataId)) { logger.LogDebug("Interacted with delivery NPC {DataId}", base.Task.NpcDataId); SetPhase(EPhase.WaitForAddon); } break; case EPhase.WaitForAddon: if (_throttle.TryReset(0.5) && !TryDismissResultAddon()) { if (SatisfactionSupplyActions.IsSatisfactionSupplyFullyReady(gameGui)) { logger.LogDebug("SatisfactionSupply addon fully ready"); SetPhase(EPhase.SelectSlot); } else if (VendorPurchaseActions.IsDialogReady(gameGui)) { VendorPurchaseActions.TrySelectDialogOption(gameGui, 0); logger.LogDebug("Dismissed NPC dialog to reach SatisfactionSupply"); } else if (SatisfactionSupplyActions.TryProgressTalk(gameGui)) { logger.LogDebug("Progressed NPC dialog to reach SatisfactionSupply"); } } break; case EPhase.SelectSlot: if (_throttle.TryReset(0.5)) { if (!PlanStillMatchesRequest()) { return ETaskResult.RetryStep; } if (SatisfactionSupplyActions.IsNpcTradeReady()) { logger.LogDebug("NpcTrade agent ready"); SetPhase(EPhase.ConfirmTrade); } else if (TryDismissResultAddon()) { SetPhase(EPhase.InteractWithNpc); } else if (SatisfactionSupplyActions.IsSatisfactionSupplyFullyReady(gameGui)) { SatisfactionSupplyActions.TrySelectDeliverySlot(gameGui, (int)base.Task.Slot); logger.LogDebug("Fired delivery slot {Slot} selection", base.Task.Slot); } } break; case EPhase.ConfirmTrade: if (_throttle.TryReset(0.5)) { if (!HasRemainingItems()) { logger.LogWarning("Planned item {ItemId} is no longer available for turn-in", base.Task.ItemId); return ETaskResult.RetryStep; } if (SatisfactionSupplyActions.TryConfirmTrade()) { logger.LogDebug("Confirmed trade for slot {Slot}", base.Task.Slot); SetPhase(EPhase.WaitForCutsceneStart); } } break; case EPhase.WaitForCutsceneStart: if (HasDeliveryRegistered()) { logger.LogDebug("Delivery registered before cutscene state was observed"); SetPhase(EPhase.CheckCompletion); } else if (condition[ConditionFlag.OccupiedInCutSceneEvent]) { logger.LogDebug("Cutscene started"); SetPhase(EPhase.WaitForCutsceneEnd); } break; case EPhase.WaitForCutsceneEnd: if (!condition[ConditionFlag.OccupiedInCutSceneEvent]) { logger.LogDebug("Cutscene ended"); SetPhase(EPhase.CheckCompletion); } break; case EPhase.CheckCompletion: { int usedDeliveries = SatisfactionSupplyActions.GetNpcState(base.Task.NpcIndex).UsedDeliveries; if (usedDeliveries > _lastUsedDeliveries) { _remaining = Math.Max(0, _remaining - (usedDeliveries - _lastUsedDeliveries)); _lastUsedDeliveries = usedDeliveries; int remainingAllowances = SatisfactionSupplyActions.GetRemainingAllowances(); logger.LogDebug("Delivery complete, remaining={Remaining}, allowances={Allowances}", _remaining, remainingAllowances); if (_remaining > 0 && remainingAllowances > 0) { SetPhase(EPhase.WaitForAddon); } else { SetPhase(EPhase.Closing); } } break; } case EPhase.Closing: if (_throttle.TryReset(0.5) && !TryDismissResultAddon()) { if (SatisfactionSupplyActions.TryCloseSatisfactionSupply(gameGui)) { logger.LogDebug("Closed SatisfactionSupply addon"); return ETaskResult.TaskComplete; } if (!SatisfactionSupplyActions.IsSatisfactionSupplyReady(gameGui)) { logger.LogDebug("SatisfactionSupply addon already closed"); return ETaskResult.TaskComplete; } } break; } return ETaskResult.StillRunning; } private bool TryDismissResultAddon() { if (!SatisfactionSupplyActions.IsSatisfactionSupplyResultVisible(gameGui)) { return false; } SatisfactionSupplyActions.TryCloseSatisfactionSupplyResult(gameGui); logger.LogDebug("Closed rank-up result addon"); return true; } private void SetPhase(EPhase phase) { _phase = phase; _phaseStartMs = Environment.TickCount64; } private bool HasDeliveryRegistered() { return SatisfactionSupplyActions.GetNpcState(base.Task.NpcIndex).UsedDeliveries > _lastUsedDeliveries; } private unsafe bool PlanStillMatchesRequest() { AgentSatisfactionSupply* ptr = AgentSatisfactionSupply.Instance(); if (ptr == null || !ptr->IsAgentActive() || !ptr->NpcInfo.Valid || !ptr->NpcInfo.Initialized) { return true; } uint id = ptr->Items[(int)base.Task.Slot].Id; if (ptr->NpcInfo.Id == (uint)(base.Task.NpcIndex + 1) && id == base.Task.ItemId && HasRemainingItems()) { return true; } logger.LogWarning("Turn-in plan no longer matches live request: NPC {NpcIndex}, slot {Slot}, planned item {PlannedItemId}, live item {LiveItemId}", base.Task.NpcIndex, base.Task.Slot, base.Task.ItemId, id); return false; } private unsafe bool HasRemainingItems() { InventoryManager* ptr = InventoryManager.Instance(); if (ptr != null) { return ptr->GetInventoryItemCount(base.Task.ItemId, isHq: false, checkEquipped: true, checkArmory: true, (short)base.Task.Collectability) >= _remaining; } return false; } public override bool ShouldInterruptOnDamage() { return false; } }