muffin v7.4.10

This commit is contained in:
alydev 2026-01-19 08:31:23 +10:00
parent 2df81c5d15
commit b8dd142c23
47 changed files with 3604 additions and 1058 deletions

View file

@ -0,0 +1,12 @@
namespace LLib.Shop.Model;
public sealed class GrandCompanyItem
{
public required int Index { get; init; }
public required uint ItemId { get; init; }
public required uint IconId { get; init; }
public required uint SealCost { get; init; }
}

View file

@ -0,0 +1,288 @@
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);
}
}

View file

@ -0,0 +1,16 @@
using System;
namespace LLib.Shop;
public sealed class PurchaseCompletedEventArgs : EventArgs
{
public uint ItemId { get; }
public bool Success { get; }
public PurchaseCompletedEventArgs(uint itemId, bool success)
{
ItemId = itemId;
Success = success;
}
}