qstbak/LLib/LLib.Shop/RegularShopBase.cs
2026-08-17 20:29:32 +10:00

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);
}
}