qstbak/Questionable/Questionable.Controller.CustomDelivery/SatisfactionSupplyTurnInExecutor.cs
2026-08-19 13:19:57 +10:00

239 lines
7 KiB
C#

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<SatisfactionSupplyTurnInExecutor> logger) : TaskExecutor<SatisfactionSupplyTurnInTask>()
{
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;
}
}