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