1
0
Fork 0
forked from aly/qstbak
qstbak/Questionable/Questionable.Controller.CustomDelivery/VendorPurchaseExecutor.cs
2026-08-17 20:29:32 +10:00

223 lines
6.5 KiB
C#

using System;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using LLib;
using LLib.Shop;
using Microsoft.Extensions.Logging;
using Questionable.Controller.Steps;
using Questionable.Functions;
namespace Questionable.Controller.CustomDelivery;
internal sealed class VendorPurchaseExecutor(GameFunctions gameFunctions, IGameGui gameGui, ICondition condition, ILogger<VendorPurchaseExecutor> logger) : TaskExecutor<VendorPurchaseTask>()
{
private enum EState
{
InteractWithNpc,
WaitForShopOrDialog,
BuyItem,
WaitForBuy,
CloseShop,
WaitForClose
}
private const long StateTimeoutMs = 15000L;
private const long BuyConfirmationTimeoutMs = 2000L;
private const int MaxBuyAttempts = 3;
private EState _state;
private Throttle _throttle;
private int _purchaseIndex;
private long _stateStartMs;
private bool _failed;
private int _countBeforeBuy;
private int _buyAttempts;
private bool _confirmationClicked;
protected override bool Start()
{
_state = EState.InteractWithNpc;
_throttle.Clear();
_purchaseIndex = 0;
_stateStartMs = Environment.TickCount64;
_failed = false;
_countBeforeBuy = 0;
_buyAttempts = 0;
_confirmationClicked = false;
return true;
}
public unsafe override ETaskResult Update()
{
long num = Environment.TickCount64 - _stateStartMs;
if (_state != EState.WaitForBuy && num > 15000)
{
logger.LogWarning("Vendor purchase timed out in state {State} after {Elapsed}ms, retrying", _state, num);
return ETaskResult.RetryStep;
}
switch (_state)
{
case EState.InteractWithNpc:
if (!_throttle.TryReset(1.0))
{
return ETaskResult.StillRunning;
}
if (gameFunctions.InteractWith(base.Task.VendorNpcId))
{
logger.LogDebug("Interacted with vendor NPC {NpcId}", base.Task.VendorNpcId);
SetState(EState.WaitForShopOrDialog);
}
return ETaskResult.StillRunning;
case EState.WaitForShopOrDialog:
if (VendorPurchaseActions.IsShopReady(gameGui))
{
logger.LogDebug("Shop is ready, moving to BuyItem");
SetState(EState.BuyItem);
}
else if (VendorPurchaseActions.IsDialogReady(gameGui))
{
if (!_throttle.TryReset(0.5))
{
return ETaskResult.StillRunning;
}
if (VendorPurchaseActions.TrySelectDialogOption(gameGui, base.Task.DialogOptionIndex))
{
logger.LogDebug("Selected dialog option {Index} to open shop", base.Task.DialogOptionIndex);
}
}
return ETaskResult.StillRunning;
case EState.BuyItem:
{
if (!_throttle.TryReset(0.5))
{
return ETaskResult.StillRunning;
}
VendorPurchaseTask.PurchaseItem purchaseItem2 = base.Task.Purchases[_purchaseIndex];
int num2 = VendorPurchaseActions.FindItemDisplayIndex(gameGui, purchaseItem2.ItemId);
if (num2 < 0)
{
logger.LogError("Item {ItemId} not found in shop display list", purchaseItem2.ItemId);
_failed = true;
SetState(EState.CloseShop);
return ETaskResult.StillRunning;
}
_countBeforeBuy = InventoryManager.Instance()->GetInventoryItemCount(purchaseItem2.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0);
if (VendorPurchaseActions.TryBuyItem(gameGui, num2, purchaseItem2.Quantity))
{
_buyAttempts++;
_confirmationClicked = false;
logger.LogDebug("Submitted purchase for item {ItemId} (display index {DisplayIndex}) x{Quantity}, attempt {Attempt}/{MaxAttempts} ({Current}/{Total})", purchaseItem2.ItemId, num2, purchaseItem2.Quantity, _buyAttempts, 3, _purchaseIndex + 1, base.Task.Purchases.Count);
SetState(EState.WaitForBuy);
}
return ETaskResult.StillRunning;
}
case EState.WaitForBuy:
{
VendorPurchaseTask.PurchaseItem purchaseItem = base.Task.Purchases[_purchaseIndex];
if (!_confirmationClicked && VendorPurchaseActions.TryConfirmPurchase(gameGui))
{
_confirmationClicked = true;
logger.LogDebug("Confirmed purchase of item {ItemId}", purchaseItem.ItemId);
SetState(EState.WaitForBuy);
return ETaskResult.StillRunning;
}
int inventoryItemCount = InventoryManager.Instance()->GetInventoryItemCount(purchaseItem.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0);
if (inventoryItemCount <= _countBeforeBuy)
{
if (num < 2000)
{
return ETaskResult.StillRunning;
}
if (_buyAttempts < 3)
{
logger.LogWarning("Purchase of item {ItemId} was not reflected in inventory; retrying ({Attempt}/{MaxAttempts})", purchaseItem.ItemId, _buyAttempts + 1, 3);
SetState(EState.BuyItem);
return ETaskResult.StillRunning;
}
logger.LogError("Purchase of item {ItemId} failed after {Attempts} attempts", purchaseItem.ItemId, _buyAttempts);
_failed = true;
SetState(EState.CloseShop);
return ETaskResult.StillRunning;
}
if (!_throttle.TryReset(0.5))
{
return ETaskResult.StillRunning;
}
logger.LogDebug("Verified purchase of item {ItemId}: inventory {Before} -> {After}", purchaseItem.ItemId, _countBeforeBuy, inventoryItemCount);
_purchaseIndex++;
if (_purchaseIndex < base.Task.Purchases.Count)
{
_buyAttempts = 0;
logger.LogDebug("Buying next item ({Index}/{Total})", _purchaseIndex + 1, base.Task.Purchases.Count);
SetState(EState.BuyItem);
}
else
{
logger.LogDebug("All purchases complete, closing shop");
SetState(EState.CloseShop);
}
return ETaskResult.StillRunning;
}
case EState.CloseShop:
if (!_throttle.TryReset(0.5))
{
return ETaskResult.StillRunning;
}
if (VendorPurchaseActions.TryCloseShop(gameGui))
{
logger.LogDebug("Closing shop");
SetState(EState.WaitForClose);
}
return ETaskResult.StillRunning;
case EState.WaitForClose:
if (VendorPurchaseActions.IsShopReady(gameGui))
{
return ETaskResult.StillRunning;
}
if (VendorPurchaseActions.IsDialogReady(gameGui))
{
if (!_throttle.TryReset(0.5))
{
return ETaskResult.StillRunning;
}
VendorPurchaseActions.TrySelectDialogOption(gameGui, -1);
logger.LogDebug("Dismissing post-shop dialog");
return ETaskResult.StillRunning;
}
if (!condition[ConditionFlag.OccupiedInEvent])
{
if (_failed)
{
logger.LogWarning("Shop closed after failed purchase, retrying");
return ETaskResult.RetryStep;
}
logger.LogDebug("Shop closed, vendor purchase complete");
return ETaskResult.TaskComplete;
}
return ETaskResult.StillRunning;
default:
return ETaskResult.StillRunning;
}
}
private void SetState(EState state)
{
_state = state;
_stateStartMs = Environment.TickCount64;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}