qstbak/Questionable/Questionable.Controller.GameUi/ShopController.cs
2026-08-17 20:29:32 +10:00

220 lines
5.2 KiB
C#

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<ShopController> _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<ShopController> 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<AtkUnitBase>("Shop", out var addonPtr))
{
addonPtr->FireCallbackInt(-1);
}
}
}