288 lines
8.6 KiB
C#
288 lines
8.6 KiB
C#
using System;
|
|
using Dalamud.Game.Addon.Lifecycle;
|
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|
using Dalamud.Plugin.Services;
|
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
using FFXIVClientStructs.FFXIV.Client.Game.UI;
|
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
|
using LLib.GameUI;
|
|
using LLib.Shop.Model;
|
|
|
|
namespace LLib.Shop;
|
|
|
|
public sealed class GrandCompanyShop : IDisposable
|
|
{
|
|
private sealed class GrandCompanyPurchaseRequest
|
|
{
|
|
public required uint ItemId { get; init; }
|
|
|
|
public required int RankTabIndex { get; init; }
|
|
|
|
public required int CategoryTabIndex { get; init; }
|
|
}
|
|
|
|
private const string AddonName = "GrandCompanyExchange";
|
|
|
|
private const string SelectYesNoAddonName = "SelectYesno";
|
|
|
|
private readonly IPluginLog _pluginLog;
|
|
|
|
private readonly IGameGui _gameGui;
|
|
|
|
private readonly IAddonLifecycle _addonLifecycle;
|
|
|
|
private int _navigationStep;
|
|
|
|
private DateTime _lastActionTime = DateTime.MinValue;
|
|
|
|
private GrandCompanyPurchaseRequest? _pendingPurchase;
|
|
|
|
public bool IsOpen { get; private set; }
|
|
|
|
public bool IsPurchaseInProgress => _pendingPurchase != null;
|
|
|
|
public bool IsAwaitingConfirmation { get; private set; }
|
|
|
|
public event EventHandler<PurchaseCompletedEventArgs>? PurchaseCompleted;
|
|
|
|
public event EventHandler? ShopOpened;
|
|
|
|
public event EventHandler? ShopClosed;
|
|
|
|
public GrandCompanyShop(IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle)
|
|
{
|
|
_pluginLog = pluginLog;
|
|
_gameGui = gameGui;
|
|
_addonLifecycle = addonLifecycle;
|
|
_addonLifecycle.RegisterListener(AddonEvent.PostSetup, "GrandCompanyExchange", OnShopPostSetup);
|
|
_addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "GrandCompanyExchange", OnShopPreFinalize);
|
|
_addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "SelectYesno", OnSelectYesNoPostUpdate);
|
|
}
|
|
|
|
private void OnShopPostSetup(AddonEvent type, AddonArgs args)
|
|
{
|
|
IsOpen = true;
|
|
_navigationStep = 0;
|
|
_pluginLog.Debug("[GCShop] Shop opened");
|
|
this.ShopOpened?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
private void OnShopPreFinalize(AddonEvent type, AddonArgs args)
|
|
{
|
|
IsOpen = false;
|
|
_pendingPurchase = null;
|
|
IsAwaitingConfirmation = false;
|
|
_pluginLog.Debug("[GCShop] Shop closed");
|
|
this.ShopClosed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
|
|
private unsafe void OnSelectYesNoPostUpdate(AddonEvent type, AddonArgs args)
|
|
{
|
|
if (_pendingPurchase != null && IsAwaitingConfirmation)
|
|
{
|
|
AtkUnitBase* address = (AtkUnitBase*)args.Addon.Address;
|
|
if (LAddon.IsAddonReady(address))
|
|
{
|
|
_pluginLog.Information("[GCShop] Confirming purchase dialog");
|
|
ClickYes(address);
|
|
address->Close(fireCallback: true);
|
|
IsAwaitingConfirmation = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool StartPurchase(uint itemId, int rankTabIndex, int categoryTabIndex)
|
|
{
|
|
if (!IsOpen || _pendingPurchase != null)
|
|
{
|
|
return false;
|
|
}
|
|
_pendingPurchase = new GrandCompanyPurchaseRequest
|
|
{
|
|
ItemId = itemId,
|
|
RankTabIndex = rankTabIndex,
|
|
CategoryTabIndex = categoryTabIndex
|
|
};
|
|
_navigationStep = 0;
|
|
_lastActionTime = DateTime.MinValue;
|
|
_pluginLog.Information($"[GCShop] Starting purchase of item {itemId}");
|
|
return true;
|
|
}
|
|
|
|
public void CancelPurchase()
|
|
{
|
|
_pendingPurchase = null;
|
|
IsAwaitingConfirmation = false;
|
|
_navigationStep = 0;
|
|
}
|
|
|
|
public unsafe bool ProcessPurchase()
|
|
{
|
|
if (_pendingPurchase == null || !IsOpen)
|
|
{
|
|
return false;
|
|
}
|
|
if ((DateTime.Now - _lastActionTime).TotalMilliseconds < 600.0)
|
|
{
|
|
return false;
|
|
}
|
|
if (InventoryManager.Instance()->GetInventoryItemCount(_pendingPurchase.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0)
|
|
{
|
|
_pluginLog.Information($"[GCShop] Item {_pendingPurchase.ItemId} already in inventory");
|
|
uint itemId = _pendingPurchase.ItemId;
|
|
_pendingPurchase = null;
|
|
this.PurchaseCompleted?.Invoke(this, new PurchaseCompletedEventArgs(itemId, success: true));
|
|
return true;
|
|
}
|
|
if (!_gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonPtr) || !LAddon.IsAddonReady(addonPtr))
|
|
{
|
|
return false;
|
|
}
|
|
switch (_navigationStep)
|
|
{
|
|
case 0:
|
|
FireCallback(addonPtr, 1, _pendingPurchase.RankTabIndex);
|
|
_navigationStep++;
|
|
_lastActionTime = DateTime.Now;
|
|
break;
|
|
case 1:
|
|
FireCallback(addonPtr, 2, _pendingPurchase.CategoryTabIndex);
|
|
_navigationStep++;
|
|
_lastActionTime = DateTime.Now;
|
|
break;
|
|
case 2:
|
|
{
|
|
GrandCompanyItem grandCompanyItem = FindItemInShop(addonPtr, _pendingPurchase.ItemId);
|
|
if (grandCompanyItem != null)
|
|
{
|
|
_pluginLog.Information($"[GCShop] Found item {grandCompanyItem.ItemId} at index {grandCompanyItem.Index}, purchasing...");
|
|
FirePurchaseCallback(addonPtr, grandCompanyItem);
|
|
IsAwaitingConfirmation = true;
|
|
_navigationStep++;
|
|
_lastActionTime = DateTime.Now;
|
|
}
|
|
else
|
|
{
|
|
_pluginLog.Warning($"[GCShop] Item {_pendingPurchase.ItemId} not found in shop");
|
|
_lastActionTime = DateTime.Now;
|
|
}
|
|
break;
|
|
}
|
|
case 3:
|
|
if (!IsAwaitingConfirmation && InventoryManager.Instance()->GetInventoryItemCount(_pendingPurchase.ItemId, isHq: false, checkEquipped: true, checkArmory: true, 0) > 0)
|
|
{
|
|
_pluginLog.Information($"[GCShop] Purchase of {_pendingPurchase.ItemId} completed");
|
|
uint itemId2 = _pendingPurchase.ItemId;
|
|
_pendingPurchase = null;
|
|
_navigationStep = 0;
|
|
this.PurchaseCompleted?.Invoke(this, new PurchaseCompletedEventArgs(itemId2, success: true));
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
public unsafe void CloseShop()
|
|
{
|
|
if (IsOpen && _gameGui.TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonPtr))
|
|
{
|
|
addonPtr->Close(fireCallback: true);
|
|
}
|
|
}
|
|
|
|
public unsafe static int GetCompanySeals()
|
|
{
|
|
return (int)InventoryManager.Instance()->GetCompanySeals(PlayerState.Instance()->GrandCompany);
|
|
}
|
|
|
|
private unsafe static GrandCompanyItem? FindItemInShop(AtkUnitBase* addon, uint targetItemId)
|
|
{
|
|
int atkValueInt = GetAtkValueInt(addon, 1);
|
|
if (atkValueInt <= 0)
|
|
{
|
|
return null;
|
|
}
|
|
for (int i = 0; i < atkValueInt; i++)
|
|
{
|
|
int atkValueInt2 = GetAtkValueInt(addon, 317 + i);
|
|
if (atkValueInt2 == (int)targetItemId)
|
|
{
|
|
return new GrandCompanyItem
|
|
{
|
|
Index = i,
|
|
ItemId = (uint)atkValueInt2,
|
|
IconId = (uint)GetAtkValueInt(addon, 167 + i),
|
|
SealCost = (uint)GetAtkValueInt(addon, 67 + i)
|
|
};
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private unsafe void FireCallback(AtkUnitBase* addon, params int[] args)
|
|
{
|
|
AtkValue* ptr = stackalloc AtkValue[args.Length];
|
|
for (int i = 0; i < args.Length; i++)
|
|
{
|
|
ptr[i].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[i].Int = args[i];
|
|
}
|
|
addon->FireCallback((uint)args.Length, ptr);
|
|
}
|
|
|
|
private unsafe void FirePurchaseCallback(AtkUnitBase* addon, GrandCompanyItem item)
|
|
{
|
|
AtkValue* ptr = stackalloc AtkValue[9];
|
|
ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr->Int = 0;
|
|
ptr[1].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[1].Int = item.Index;
|
|
ptr[2].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[2].Int = 1;
|
|
ptr[3].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[3].Int = 0;
|
|
ptr[4].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[4].Int = 0;
|
|
ptr[5].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr[5].Int = 0;
|
|
ptr[6].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt;
|
|
ptr[6].UInt = item.ItemId;
|
|
ptr[7].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt;
|
|
ptr[7].UInt = item.IconId;
|
|
ptr[8].Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt;
|
|
ptr[8].UInt = item.SealCost;
|
|
addon->FireCallback(9u, ptr);
|
|
}
|
|
|
|
private unsafe static void ClickYes(AtkUnitBase* addon)
|
|
{
|
|
AtkValue* ptr = stackalloc AtkValue[1];
|
|
ptr->Type = FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int;
|
|
ptr->Int = 0;
|
|
addon->FireCallback(1u, ptr);
|
|
}
|
|
|
|
private unsafe static int GetAtkValueInt(AtkUnitBase* addon, int index)
|
|
{
|
|
if (addon == null || addon->AtkValues == null || index < 0 || index >= addon->AtkValuesCount)
|
|
{
|
|
return -1;
|
|
}
|
|
AtkValue atkValue = addon->AtkValues[index];
|
|
return atkValue.Type switch
|
|
{
|
|
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Int => atkValue.Int,
|
|
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.UInt => (int)atkValue.UInt,
|
|
FFXIVClientStructs.FFXIV.Component.GUI.ValueType.Bool => (atkValue.Byte != 0) ? 1 : 0,
|
|
_ => -1,
|
|
};
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "GrandCompanyExchange", OnShopPostSetup);
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "GrandCompanyExchange", OnShopPreFinalize);
|
|
_addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "SelectYesno", OnSelectYesNoPostUpdate);
|
|
}
|
|
}
|