using System; using System.Linq; using System.Numerics; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Component.GUI; using LLib.GameUI; using LLib.Shop; using LLib.Shop.Model; using Microsoft.Extensions.Logging; using Questionable.Model.Questing; namespace Questionable.Controller.GameUi; internal sealed class ShopController : IDisposable, IShopWindow { private readonly QuestController _questController; private readonly IGameGui _gameGui; private readonly IFramework _framework; private readonly RegularShopBase _shop; private readonly ILogger _logger; private long _lastCurrentStockSwitchMs; private int _currentStockSwitchAttempts; private uint? _lastPurchaseItemId; private const int MaxCurrentStockSwitchAttempts = 3; public bool IsEnabled => _questController.IsRunning; public bool IsOpen { get; set; } public bool IsAutoBuyEnabled => _shop.AutoBuyEnabled; public bool IsAwaitingYesNo { get { return _shop.IsAwaitingYesNo; } set { _shop.IsAwaitingYesNo = value; } } public Vector2? Position { get; set; } public ShopController(QuestController questController, IGameGui gameGui, IAddonLifecycle addonLifecycle, IFramework framework, ILogger logger, IPluginLog pluginLog, IDataManager dataManager) { _questController = questController; _gameGui = gameGui; _framework = framework; _shop = new RegularShopBase(this, "Shop", pluginLog, gameGui, addonLifecycle, dataManager); _logger = logger; _framework.Update += FrameworkUpdate; } public void Dispose() { _framework.Update -= FrameworkUpdate; _shop.Dispose(); } private void FrameworkUpdate(IFramework framework) { if (!IsOpen || _shop.ItemForSale == null) { return; } if (_shop.PurchaseState != null) { _shop.HandleNextPurchaseStep(); return; } QuestStep questStep = FindCurrentStep(); if (questStep == null || questStep.InteractionType != EInteractionType.PurchaseItem) { _shop.ItemForSale = null; RestoreExternalPluginState(); return; } int val = Math.Max(0, questStep.ItemCount.GetValueOrDefault() - (int)_shop.ItemForSale.OwnedItems); int num = Math.Min(_shop.GetMaxItemsToPurchase(), val); if (num > 0) { _logger.LogDebug("Auto-buying {ToPurchase} {ItemName}", num, _shop.ItemForSale.ItemName); _shop.StartAutoPurchase(num); _shop.HandleNextPurchaseStep(); } else { _shop.ItemForSale = null; RestoreExternalPluginState(); } } private QuestStep? FindCurrentStep() { QuestController.QuestProgress currentQuest = _questController.CurrentQuest; return (currentQuest?.Quest.FindSequence(currentQuest.Sequence))?.FindStep(currentQuest?.Step ?? 0); } public unsafe void UpdateShopStock(AtkUnitBase* addon) { QuestStep currentStep = FindCurrentStep(); if (currentStep == null || currentStep.InteractionType != EInteractionType.PurchaseItem) { _shop.ItemForSale = null; return; } if (addon->AtkValuesCount != 625) { _logger.LogError("Unexpected amount of atkvalues for Shop addon ({AtkValueCount})", addon->AtkValuesCount); _shop.ItemForSale = null; return; } AtkValue* atkValues = addon->AtkValues; if (currentStep.ItemId != _lastPurchaseItemId) { _lastPurchaseItemId = currentStep.ItemId; _currentStockSwitchAttempts = 0; } uint uInt = atkValues[2].UInt; if (uInt == 0) { _shop.ItemForSale = null; return; } _shop.ItemForSale = (from i in Enumerable.Range(0, (int)uInt) select new ItemForSale { Position = i, ItemName = atkValues[14 + i].ReadAtkString(), Price = atkValues[75 + i].UInt, CurrencyItemId = 1u, OwnedItems = atkValues[136 + i].UInt, ItemId = atkValues[441 + i].UInt }).FirstOrDefault((ItemForSale x) => x.ItemId == currentStep.ItemId); if (_shop.ItemForSale != null) { _currentStockSwitchAttempts = 0; } else if (_currentStockSwitchAttempts < 3 && Environment.TickCount64 - _lastCurrentStockSwitchMs > 2000) { _currentStockSwitchAttempts++; _lastCurrentStockSwitchMs = Environment.TickCount64; _logger.LogWarning("Shop list has {ItemCount} item(s) but not {ItemId}; switching to Current Stock tab ({Attempt}/{Max})", uInt, currentStep.ItemId, _currentStockSwitchAttempts, 3); SwitchToCurrentStockTab(addon); } } public unsafe void TriggerPurchase(AtkUnitBase* addonShop, int buyNow) { AtkValue* values = stackalloc AtkValue[4] { new AtkValue { Type = AtkValueType.Int, Int = 0 }, new AtkValue { Type = AtkValueType.Int, Int = _shop.ItemForSale.Position }, new AtkValue { Type = AtkValueType.Int, Int = buyNow }, new AtkValue { Type = AtkValueType.Undefined, Int = 0 } }; addonShop->FireCallback(4u, values); } private unsafe void SwitchToCurrentStockTab(AtkUnitBase* addonShop) { AtkValue* values = stackalloc AtkValue[3] { new AtkValue { Type = AtkValueType.Int, Int = 1 }, new AtkValue { Type = AtkValueType.Int, Int = -1 }, new AtkValue { Type = AtkValueType.Int, Int = -1 } }; addonShop->FireCallback(3u, values); } public void SaveExternalPluginState() { } public unsafe void RestoreExternalPluginState() { if (_gameGui.TryGetAddonByName("Shop", out var addonPtr)) { addonPtr->FireCallbackInt(-1); } } }