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