239 lines
7 KiB
C#
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;
|
|
}
|
|
}
|