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 _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? 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(); _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(_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); } }