253 lines
7.1 KiB
C#
253 lines
7.1 KiB
C#
using System;
|
|
using System.Numerics;
|
|
using Dalamud.Game.Addon.Lifecycle;
|
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using LLib.GameUI;
|
|
using LLib.Inventory;
|
|
using LLib.Shop.Model;
|
|
using Lumina.Excel;
|
|
using Lumina.Excel.Sheets;
|
|
|
|
namespace LLib.Shop;
|
|
|
|
public class RegularShopBase : IDisposable
|
|
{
|
|
private readonly IShopWindow _parentWindow;
|
|
|
|
private readonly string _addonName;
|
|
|
|
private readonly IPluginLog _pluginLog;
|
|
|
|
private readonly IGameGui _gameGui;
|
|
|
|
private readonly IAddonLifecycle _addonLifecycle;
|
|
|
|
private readonly ExcelSheet<Item> _itemSheet;
|
|
|
|
private long _purchaseDeadlineMs;
|
|
|
|
private const int MaxPurchaseQuantity = 99;
|
|
|
|
private const int MaxWaitMs = 3000;
|
|
|
|
public ItemForSale? ItemForSale { get; set; }
|
|
|
|
public PurchaseState? PurchaseState { get; private set; }
|
|
|
|
public bool AutoBuyEnabled => PurchaseState != null;
|
|
|
|
public bool IsAwaitingYesNo
|
|
{
|
|
get
|
|
{
|
|
return PurchaseState?.IsAwaitingYesNo ?? false;
|
|
}
|
|
set
|
|
{
|
|
if (PurchaseState != null)
|
|
{
|
|
PurchaseState.IsAwaitingYesNo = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
public event EventHandler<PurchaseCompletedEventArgs>? PurchaseCompleted;
|
|
|
|
public RegularShopBase(IShopWindow parentWindow, string addonName, IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle, IDataManager dataManager)
|
|
{
|
|
ArgumentNullException.ThrowIfNull(parentWindow, "parentWindow");
|
|
ArgumentException.ThrowIfNullOrEmpty(addonName, "addonName");
|
|
ArgumentNullException.ThrowIfNull(pluginLog, "pluginLog");
|
|
ArgumentNullException.ThrowIfNull(gameGui, "gameGui");
|
|
ArgumentNullException.ThrowIfNull(addonLifecycle, "addonLifecycle");
|
|
ArgumentNullException.ThrowIfNull(dataManager, "dataManager");
|
|
_parentWindow = parentWindow;
|
|
_addonName = addonName;
|
|
_pluginLog = pluginLog;
|
|
_gameGui = gameGui;
|
|
_addonLifecycle = addonLifecycle;
|
|
_itemSheet = dataManager.GetExcelSheet<Item>();
|
|
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
|
|
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
|
|
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
|
|
}
|
|
|
|
private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args)
|
|
{
|
|
if (!_parentWindow.IsEnabled)
|
|
{
|
|
ItemForSale = null;
|
|
_parentWindow.IsOpen = false;
|
|
return;
|
|
}
|
|
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
|
|
PostUpdateShopStock();
|
|
if (ItemForSale != null)
|
|
{
|
|
_parentWindow.IsOpen = true;
|
|
}
|
|
}
|
|
|
|
private void ShopPreFinalize(AddonEvent type, AddonArgs args)
|
|
{
|
|
if (PurchaseState != null && ItemForSale != null)
|
|
{
|
|
CompletePurchase(ItemForSale.ItemId, success: false);
|
|
}
|
|
else
|
|
{
|
|
PurchaseState = null;
|
|
_parentWindow.RestoreExternalPluginState();
|
|
}
|
|
_parentWindow.IsOpen = false;
|
|
}
|
|
|
|
private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args)
|
|
{
|
|
if (!_parentWindow.IsEnabled)
|
|
{
|
|
ItemForSale = null;
|
|
_parentWindow.IsOpen = false;
|
|
return;
|
|
}
|
|
_parentWindow.UpdateShopStock((AtkUnitBase*)args.Addon.Address);
|
|
PostUpdateShopStock();
|
|
if (ItemForSale != null)
|
|
{
|
|
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
|
|
short num = 0;
|
|
short num2 = 0;
|
|
address->GetPosition(&num, &num2);
|
|
ushort num3 = 0;
|
|
ushort num4 = 0;
|
|
address->GetSize(&num3, &num4, scaled: true);
|
|
num += (short)num3;
|
|
Vector2? position = _parentWindow.Position;
|
|
if (position.HasValue)
|
|
{
|
|
Vector2 valueOrDefault = position.GetValueOrDefault();
|
|
if ((short)valueOrDefault.X != num || (short)valueOrDefault.Y != num2)
|
|
{
|
|
_parentWindow.Position = new Vector2(num, num2);
|
|
}
|
|
}
|
|
_parentWindow.IsOpen = true;
|
|
}
|
|
else
|
|
{
|
|
_parentWindow.IsOpen = false;
|
|
}
|
|
}
|
|
|
|
private void PostUpdateShopStock()
|
|
{
|
|
if (ItemForSale != null && PurchaseState != null)
|
|
{
|
|
int ownedItems = (int)ItemForSale.OwnedItems;
|
|
if (PurchaseState.OwnedItems != ownedItems)
|
|
{
|
|
PurchaseState.OwnedItems = ownedItems;
|
|
PurchaseState.NextStepMs = Environment.TickCount64 + 250;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int GetCurrencyCount(uint currencyItemId)
|
|
{
|
|
return InventoryHelper.GetItemCount(currencyItemId);
|
|
}
|
|
|
|
public int GetMaxItemsToPurchase()
|
|
{
|
|
if (ItemForSale == null)
|
|
{
|
|
return 0;
|
|
}
|
|
int maxHoldableQuantity = InventoryHelper.GetMaxHoldableQuantity(ItemForSale.ItemId, _itemSheet);
|
|
if (ItemForSale.Price == 0)
|
|
{
|
|
return maxHoldableQuantity;
|
|
}
|
|
return Math.Min((int)(GetCurrencyCount(ItemForSale.CurrencyItemId) / ItemForSale.Price), maxHoldableQuantity);
|
|
}
|
|
|
|
public void CancelAutoPurchase()
|
|
{
|
|
if (ItemForSale != null && PurchaseState != null)
|
|
{
|
|
CompletePurchase(ItemForSale.ItemId, success: false);
|
|
}
|
|
}
|
|
|
|
public void StartAutoPurchase(int toPurchase)
|
|
{
|
|
if (ItemForSale == null)
|
|
{
|
|
throw new InvalidOperationException("Cannot start auto-purchase without an item selected");
|
|
}
|
|
PurchaseState = new PurchaseState((int)ItemForSale.OwnedItems + toPurchase, (int)ItemForSale.OwnedItems);
|
|
_parentWindow.SaveExternalPluginState();
|
|
}
|
|
|
|
public unsafe void HandleNextPurchaseStep()
|
|
{
|
|
if (ItemForSale == null || PurchaseState == null)
|
|
{
|
|
return;
|
|
}
|
|
if (PurchaseState.IsComplete)
|
|
{
|
|
_pluginLog.Information($"Stopping item purchase (desired = {PurchaseState.DesiredItems}, owned = {PurchaseState.OwnedItems})");
|
|
CompletePurchase(ItemForSale.ItemId, success: true);
|
|
return;
|
|
}
|
|
int maxHoldableQuantity = InventoryHelper.GetMaxHoldableQuantity(ItemForSale.ItemId, _itemSheet);
|
|
int num = Math.Min(99, maxHoldableQuantity);
|
|
AtkUnitBase* addonPtr;
|
|
if (num == 0)
|
|
{
|
|
_pluginLog.Warning("No inventory space for " + ItemForSale.ItemName);
|
|
CompletePurchase(ItemForSale.ItemId, success: false);
|
|
}
|
|
else if (Environment.TickCount64 >= PurchaseState.NextStepMs && _gameGui.TryGetAddonByName<AtkUnitBase>(_addonName, out addonPtr))
|
|
{
|
|
int num2 = Math.Min(PurchaseState.ItemsLeftToBuy, num);
|
|
_pluginLog.Information($"Buying {num2}x {ItemForSale.ItemName}");
|
|
_parentWindow.TriggerPurchase(addonPtr, num2);
|
|
PurchaseState.NextStepMs = long.MaxValue;
|
|
_purchaseDeadlineMs = Environment.TickCount64 + 3000;
|
|
PurchaseState.IsAwaitingYesNo = true;
|
|
}
|
|
else if (PurchaseState.NextStepMs == long.MaxValue && Environment.TickCount64 >= _purchaseDeadlineMs)
|
|
{
|
|
_pluginLog.Warning($"Purchase of {ItemForSale.ItemName} did not progress within {3000}ms, cancelling");
|
|
CompletePurchase(ItemForSale.ItemId, success: false);
|
|
}
|
|
}
|
|
|
|
private void CompletePurchase(uint itemId, bool success)
|
|
{
|
|
int quantityPurchased = ((success && PurchaseState != null) ? (PurchaseState.OwnedItems - PurchaseState.InitialOwnedItems) : 0);
|
|
PurchaseState = null;
|
|
_parentWindow.RestoreExternalPluginState();
|
|
this.PurchaseCompleted?.Invoke(this, new PurchaseCompletedEventArgs(itemId, success, quantityPurchased));
|
|
}
|
|
|
|
protected virtual void Dispose(bool disposing)
|
|
{
|
|
if (disposing)
|
|
{
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup);
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize);
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate);
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Dispose(disposing: true);
|
|
GC.SuppressFinalize(this);
|
|
}
|
|
}
|