From f3e0d8f17c92bbe9138cf084583c47ebb81fd93c Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 25 Oct 2023 00:19:42 +0200 Subject: [PATCH] Experimental Ceruleum Tank calculator --- LLib | 2 +- Workshoppa/Configuration.cs | 1 + Workshoppa/GameData/GameStrings.cs | 9 +- Workshoppa/GameData/Shops/ItemForSale.cs | 10 + Workshoppa/GameData/Shops/PurchaseState.cs | 19 ++ Workshoppa/Windows/CeruleumTankWindow.cs | 155 ++++++++++++++ Workshoppa/Windows/ConfigWindow.cs | 7 + Workshoppa/Windows/RepairKitWindow.cs | 222 +++------------------ Workshoppa/Windows/ShopWindow.cs | 197 ++++++++++++++++++ Workshoppa/WorkshopPlugin.GameFunctions.cs | 11 +- Workshoppa/WorkshopPlugin.SelectYesNo.cs | 16 +- Workshoppa/WorkshopPlugin.cs | 10 +- Workshoppa/Workshoppa.csproj | 2 +- 13 files changed, 452 insertions(+), 209 deletions(-) create mode 100644 Workshoppa/GameData/Shops/ItemForSale.cs create mode 100644 Workshoppa/GameData/Shops/PurchaseState.cs create mode 100644 Workshoppa/Windows/CeruleumTankWindow.cs create mode 100644 Workshoppa/Windows/ShopWindow.cs diff --git a/LLib b/LLib index 7649b0d..8944883 160000 --- a/LLib +++ b/LLib @@ -1 +1 @@ -Subproject commit 7649b0d51b35c993839b918805718f046f06ae9b +Subproject commit 89448838a1295041293bbd5dd69501ad934bdf03 diff --git a/Workshoppa/Configuration.cs b/Workshoppa/Configuration.cs index fb28353..9adfd5a 100644 --- a/Workshoppa/Configuration.cs +++ b/Workshoppa/Configuration.cs @@ -13,6 +13,7 @@ internal sealed class Configuration : IPluginConfiguration public CurrentItem? CurrentlyCraftedItem { get; set; } = null; public List ItemQueue { get; set; } = new(); public bool EnableRepairKitCalculator { get; set; } = true; + public bool EnableCeruleumTankCalculator { get; set; } = true; internal sealed class QueuedItem { diff --git a/Workshoppa/GameData/GameStrings.cs b/Workshoppa/GameData/GameStrings.cs index 8209221..3d83d61 100644 --- a/Workshoppa/GameData/GameStrings.cs +++ b/Workshoppa/GameData/GameStrings.cs @@ -12,8 +12,10 @@ internal sealed class GameStrings { public GameStrings(IDataManager dataManager, IPluginLog pluginLog) { - PurchaseItem = dataManager.GetRegex(3406, addon => addon.Text, pluginLog) - ?? throw new Exception($"Unable to resolve {nameof(PurchaseItem)}"); + PurchaseItemForGil = dataManager.GetRegex(3406, addon => addon.Text, pluginLog) + ?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForGil)}"); + PurchaseItemForCompanyCredits = dataManager.GetRegex(3473, addon => addon.Text, pluginLog) + ?? throw new Exception($"Unable to resolve {nameof(PurchaseItemForCompanyCredits)}"); ViewCraftingLog = dataManager.GetString("TEXT_CMNDEFCOMPANYMANUFACTORY_00150_MENU_CC_NOTE", pluginLog) ?? throw new Exception($"Unable to resolve {nameof(ViewCraftingLog)}"); @@ -26,7 +28,8 @@ internal sealed class GameStrings ?? throw new Exception($"Unable to resolve {nameof(RetrieveFinishedItem)}"); } - public Regex PurchaseItem { get; } + public Regex PurchaseItemForGil { get; } + public Regex PurchaseItemForCompanyCredits { get; } public string ViewCraftingLog { get; } public string TurnInHighQualityItem { get; } public Regex ContributeItems { get; } diff --git a/Workshoppa/GameData/Shops/ItemForSale.cs b/Workshoppa/GameData/Shops/ItemForSale.cs new file mode 100644 index 0000000..ab012d1 --- /dev/null +++ b/Workshoppa/GameData/Shops/ItemForSale.cs @@ -0,0 +1,10 @@ +namespace Workshoppa.GameData.Shops; + +internal sealed class ItemForSale +{ + public required int Position { get; init; } + public required uint ItemId { get; init; } + public required string? ItemName { get; init; } + public required uint Price { get; init; } + public required uint OwnedItems { get; init; } +} diff --git a/Workshoppa/GameData/Shops/PurchaseState.cs b/Workshoppa/GameData/Shops/PurchaseState.cs new file mode 100644 index 0000000..36245df --- /dev/null +++ b/Workshoppa/GameData/Shops/PurchaseState.cs @@ -0,0 +1,19 @@ +using System; + +namespace Workshoppa.GameData.Shops; + +internal sealed class PurchaseState +{ + public PurchaseState(int desiredItems, int ownedItems) + { + DesiredItems = desiredItems; + OwnedItems = ownedItems; + } + + public int DesiredItems { get; } + public int OwnedItems { get; set; } + public int ItemsLeftToBuy => Math.Max(0, DesiredItems - OwnedItems); + public bool IsComplete => ItemsLeftToBuy == 0; + public bool IsAwaitingYesNo { get; set; } + public DateTime NextStep { get; set; } = DateTime.MinValue; +} diff --git a/Workshoppa/Windows/CeruleumTankWindow.cs b/Workshoppa/Windows/CeruleumTankWindow.cs new file mode 100644 index 0000000..7a48fb5 --- /dev/null +++ b/Workshoppa/Windows/CeruleumTankWindow.cs @@ -0,0 +1,155 @@ +using System; +using System.Linq; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; +using LLib; +using LLib.GameUI; +using Workshoppa.External; +using Workshoppa.GameData.Shops; +using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; + +namespace Workshoppa.Windows; + +internal sealed class CeruleumTankWindow : ShopWindow +{ + private const int CeruleumTankItemId = 10155; + + private readonly WorkshopPlugin _plugin; + private readonly DalamudPluginInterface _pluginInterface; + private readonly IPluginLog _pluginLog; + private readonly Configuration _configuration; + + private int _companyCredits; + private int _buyStackCount; + private bool _buyPartialStacks = true; + + public CeruleumTankWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IPluginLog pluginLog, + IGameGui gameGui, IAddonLifecycle addonLifecycle, Configuration configuration, + ExternalPluginHandler externalPluginHandler) + : base("Ceruleum Tanks###WorkshoppaCeruleumTankWindow", "FreeCompanyCreditShop", plugin, pluginLog, gameGui, addonLifecycle, externalPluginHandler) + { + _plugin = plugin; + _pluginInterface = pluginInterface; + _pluginLog = pluginLog; + _configuration = configuration; + } + + protected override bool Enabled => _configuration.EnableCeruleumTankCalculator; + + protected override unsafe void UpdateShopStock(AtkUnitBase* addon) + { + if (addon->AtkValuesCount != 170) + { + _pluginLog.Error($"Unexpected amount of atkvalues for FreeCompanyCreditShop addon ({addon->AtkValuesCount})"); + _companyCredits = 0; + ItemForSale = null; + return; + } + + var atkValues = addon->AtkValues; + _companyCredits = (int)atkValues[3].UInt; + + uint itemCount = atkValues[9].UInt; + if (itemCount == 0) + { + ItemForSale = null; + return; + } + ItemForSale = Enumerable.Range(0, (int)itemCount) + .Select(i => new ItemForSale + { + Position = i, + ItemName = atkValues[10 + i].ReadAtkString(), + Price = atkValues[130 + i].UInt, + OwnedItems = atkValues[90 + i].UInt, + ItemId = atkValues[30 + i].UInt, + }) + .FirstOrDefault(x => x.ItemId == CeruleumTankItemId); + } + + protected override int GetCurrencyCount() => _companyCredits; + + public override void Draw() + { + if (ItemForSale == null) + { + IsOpen = false; + return; + } + + int ceruleumTanks = GetItemCount(CeruleumTankItemId); + int freeInventorySlots = _plugin.GetFreeInventorySlots(); + + LImGui.AddPatreonIcon(_pluginInterface); + + ImGui.Text("Inventory"); + ImGui.Indent(); + ImGui.Text($"Ceruleum Tanks: {FormatStackCount(ceruleumTanks)}"); + ImGui.Text($"Free Slots: {freeInventorySlots}"); + ImGui.Unindent(); + + ImGui.Separator(); + + if (PurchaseState == null) + { + ImGui.SetNextItemWidth(100); + ImGui.InputInt("Stacks to Buy", ref _buyStackCount); + _buyStackCount = Math.Min(freeInventorySlots, Math.Max(0, _buyStackCount)); + + if (ceruleumTanks % 999 > 0) + ImGui.Checkbox($"Fill Partial Stacks (+{999 - ceruleumTanks % 999})", ref _buyPartialStacks); + } + + int missingItems = _buyStackCount * 999; + if (_buyPartialStacks && ceruleumTanks % 999 > 0) + missingItems += (999 - ceruleumTanks % 999); + + if (PurchaseState != null) + { + HandleNextPurchaseStep(); + + ImGui.Text($"Buying {FormatStackCount(PurchaseState.ItemsLeftToBuy)}..."); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy")) + CancelAutoPurchase(); + } + else + { + int toPurchase = Math.Min(GetMaxItemsToPurchase(), missingItems); + if (toPurchase > 0) + { + ImGui.Spacing(); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign, + $"Auto-Buy {FormatStackCount(toPurchase)} for {ItemForSale.Price * toPurchase:N0} CC")) + { + StartAutoPurchase(toPurchase); + HandleNextPurchaseStep(); + } + } + } + } + + private string FormatStackCount(int ceruleumTanks) + { + int fullStacks = ceruleumTanks / 999; + int partials = ceruleumTanks % 999; + string stacks = fullStacks == 1 ? "stack" : "stacks"; + if (partials > 0) + return $"{fullStacks:N0} {stacks} + {partials}"; + return $"{fullStacks:N0} {stacks}"; + } + + protected override unsafe void FirePurchaseCallback(AtkUnitBase* addonShop, int buyNow) + { + var buyItem = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.UInt, UInt = (uint)ItemForSale!.Position }, + new() { Type = ValueType.UInt, UInt = (uint)buyNow }, + }; + addonShop->FireCallback(3, buyItem); + } +} diff --git a/Workshoppa/Windows/ConfigWindow.cs b/Workshoppa/Windows/ConfigWindow.cs index c935223..75a25f4 100644 --- a/Workshoppa/Windows/ConfigWindow.cs +++ b/Workshoppa/Windows/ConfigWindow.cs @@ -30,5 +30,12 @@ internal sealed class ConfigWindow : Window _configuration.EnableRepairKitCalculator = enableRepairKitCalculator; _pluginInterface.SavePluginConfig(_configuration); } + + bool enableCeruleumTankCalculator = _configuration.EnableCeruleumTankCalculator; + if (ImGui.Checkbox("Enable Ceruleum Tank Calculator", ref enableCeruleumTankCalculator)) + { + _configuration.EnableCeruleumTankCalculator = enableCeruleumTankCalculator; + _pluginInterface.SavePluginConfig(_configuration); + } } } diff --git a/Workshoppa/Windows/RepairKitWindow.cs b/Workshoppa/Windows/RepairKitWindow.cs index ca5a141..e87f9d2 100644 --- a/Workshoppa/Windows/RepairKitWindow.cs +++ b/Workshoppa/Windows/RepairKitWindow.cs @@ -1,133 +1,53 @@ using System; using System.Linq; -using System.Numerics; -using Dalamud.Game.Addon.Lifecycle; -using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; -using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Component.GUI; using ImGuiNET; using LLib; using LLib.GameUI; using Workshoppa.External; +using Workshoppa.GameData.Shops; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; namespace Workshoppa.Windows; -internal sealed class RepairKitWindow : Window, IDisposable +internal sealed class RepairKitWindow : ShopWindow { private const int DarkMatterCluster6ItemId = 10386; - private readonly WorkshopPlugin _plugin; private readonly DalamudPluginInterface _pluginInterface; private readonly IPluginLog _pluginLog; - private readonly IGameGui _gameGui; - private readonly IAddonLifecycle _addonLifecycle; private readonly Configuration _configuration; - private readonly ExternalPluginHandler _externalPluginHandler; - - private ItemForSale? _itemForSale; - private PurchaseState? _purchaseState; public RepairKitWindow(WorkshopPlugin plugin, DalamudPluginInterface pluginInterface, IPluginLog pluginLog, IGameGui gameGui, IAddonLifecycle addonLifecycle, Configuration configuration, ExternalPluginHandler externalPluginHandler) - : base("Repair Kits###WorkshoppaRepairKitWindow") + : base("Repair Kits###WorkshoppaRepairKitWindow", "Shop", plugin, pluginLog, gameGui, addonLifecycle, externalPluginHandler) { - _plugin = plugin; _pluginInterface = pluginInterface; _pluginLog = pluginLog; - _gameGui = gameGui; - _addonLifecycle = addonLifecycle; _configuration = configuration; - _externalPluginHandler = externalPluginHandler; - - Position = new Vector2(100, 100); - PositionCondition = ImGuiCond.Always; - Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse; - - _addonLifecycle.RegisterListener(AddonEvent.PostSetup, "Shop", ShopPostSetup); - _addonLifecycle.RegisterListener(AddonEvent.PreFinalize, "Shop", ShopPreFinalize); - _addonLifecycle.RegisterListener(AddonEvent.PostUpdate, "Shop", ShopPostUpdate); } - public bool AutoBuyEnabled => _purchaseState != null; + protected override bool Enabled => _configuration.EnableRepairKitCalculator; - public bool IsAwaitingYesNo - { - get => _purchaseState?.IsAwaitingYesNo ?? false; - set => _purchaseState!.IsAwaitingYesNo = value; - } - - private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args) - { - if (!_configuration.EnableRepairKitCalculator) - { - _itemForSale = null; - IsOpen = false; - return; - } - - UpdateShopStock((AtkUnitBase*)args.Addon); - if (_itemForSale != null) - IsOpen = true; - } - - private void ShopPreFinalize(AddonEvent type, AddonArgs args) - { - _purchaseState = null; - _externalPluginHandler.Restore(); - - IsOpen = false; - } - - private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args) - { - if (!_configuration.EnableRepairKitCalculator) - { - _itemForSale = null; - IsOpen = false; - return; - } - - UpdateShopStock((AtkUnitBase*)args.Addon); - if (_itemForSale != null) - { - AtkUnitBase* addon = (AtkUnitBase*)args.Addon; - short x = 0, y = 0; - addon->GetPosition(&x, &y); - - short width = 0, height = 0; - addon->GetSize(&width, &height, true); - x += width; - - if ((short)Position!.Value.X != x || (short)Position!.Value.Y != y) - Position = new Vector2(x, y); - - IsOpen = true; - } - else - IsOpen = false; - } - - private unsafe void UpdateShopStock(AtkUnitBase* addon) + protected override unsafe void UpdateShopStock(AtkUnitBase* addon) { if (GetDarkMatterClusterCount() == 0) { - _itemForSale = null; + ItemForSale = null; return; } if (addon->AtkValuesCount != 625) { _pluginLog.Error($"Unexpected amount of atkvalues for Shop addon ({addon->AtkValuesCount})"); - _itemForSale = null; + ItemForSale = null; return; } @@ -136,18 +56,18 @@ internal sealed class RepairKitWindow : Window, IDisposable // Check if on 'Current Stock' tab? if (atkValues[0].UInt != 0) { - _itemForSale = null; + ItemForSale = null; return; } uint itemCount = atkValues[2].UInt; if (itemCount == 0) { - _itemForSale = null; + ItemForSale = null; return; } - _itemForSale = Enumerable.Range(0, (int)itemCount) + ItemForSale = Enumerable.Range(0, (int)itemCount) .Select(i => new ItemForSale { Position = i, @@ -157,40 +77,16 @@ internal sealed class RepairKitWindow : Window, IDisposable ItemId = atkValues[441 + i].UInt, }) .FirstOrDefault(x => x.ItemId == DarkMatterCluster6ItemId); - if (_itemForSale != null && _purchaseState != null) - { - int ownedItems = (int)_itemForSale.OwnedItems; - if (_purchaseState.OwnedItems != ownedItems) - { - _purchaseState.OwnedItems = ownedItems; - _purchaseState.NextStep = DateTime.Now.AddSeconds(0.25); - } - } } private int GetDarkMatterClusterCount() => GetItemCount(10335); - private int GetGil() => GetItemCount(1); - - private unsafe int GetItemCount(uint itemId) - { - InventoryManager* inventoryManager = InventoryManager.Instance(); - return inventoryManager->GetInventoryItemCount(itemId, checkEquipped: false, checkArmory: false); - } - - private int GetMaxItemsToPurchase() - { - if (_itemForSale == null) - return 0; - - int gil = GetGil(); - return (int)(gil / _itemForSale!.Price); - } + protected override int GetCurrencyCount() => GetItemCount(1); public override void Draw() { int darkMatterClusters = GetDarkMatterClusterCount(); - if (_itemForSale == null || darkMatterClusters == 0) + if (ItemForSale == null || darkMatterClusters == 0) { IsOpen = false; return; @@ -201,22 +97,19 @@ internal sealed class RepairKitWindow : Window, IDisposable ImGui.Text("Inventory"); ImGui.Indent(); ImGui.Text($"Dark Matter Clusters: {darkMatterClusters:N0}"); - ImGui.Text($"Grade 6 Dark Matter: {_itemForSale.OwnedItems:N0}"); + ImGui.Text($"Grade 6 Dark Matter: {ItemForSale.OwnedItems:N0}"); ImGui.Unindent(); - int missingItems = Math.Max(0, darkMatterClusters * 5 - (int)_itemForSale.OwnedItems); + int missingItems = Math.Max(0, darkMatterClusters * 5 - (int)ItemForSale.OwnedItems); ImGui.TextColored(missingItems == 0 ? ImGuiColors.HealerGreen : ImGuiColors.DalamudRed, $"Missing Grade 6 Dark Matter: {missingItems:N0}"); - if (_purchaseState != null) + if (PurchaseState != null) { HandleNextPurchaseStep(); if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Cancel Auto-Buy")) - { - _purchaseState = null; - _externalPluginHandler.Restore(); - } + CancelAutoPurchase(); } else { @@ -224,87 +117,24 @@ internal sealed class RepairKitWindow : Window, IDisposable if (toPurchase > 0) { if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.DollarSign, - $"Auto-Buy missing Dark Matter for {_itemForSale.Price * toPurchase:N0}{SeIconChar.Gil.ToIconString()}")) + $"Auto-Buy missing Dark Matter for {ItemForSale.Price * toPurchase:N0}{SeIconChar.Gil.ToIconString()}")) { - _purchaseState = new((int)_itemForSale.OwnedItems + toPurchase, (int)_itemForSale.OwnedItems); - _externalPluginHandler.Save(); - + StartAutoPurchase(toPurchase); HandleNextPurchaseStep(); } } } } - private unsafe void HandleNextPurchaseStep() + protected override unsafe void FirePurchaseCallback(AtkUnitBase* addonShop, int buyNow) { - if (_itemForSale == null || _purchaseState == null) - return; - - if (!_plugin.HasFreeInventorySlot()) + var buyItem = stackalloc AtkValue[] { - _pluginLog.Warning($"No free inventory slots, can't buy more {_itemForSale.ItemName}"); - _purchaseState = null; - _externalPluginHandler.Restore(); - } - else if (!_purchaseState.IsComplete) - { - if (_purchaseState.NextStep <= DateTime.Now && - _gameGui.TryGetAddonByName("Shop", out AtkUnitBase* addonShop)) - { - int buyNow = Math.Min(_purchaseState.ItemsLeftToBuy, 99); - _pluginLog.Information($"Buying {buyNow}x {_itemForSale.ItemName}"); - - var buyItem = stackalloc AtkValue[] - { - new() { Type = ValueType.Int, Int = 0 }, - new() { Type = ValueType.Int, Int = _itemForSale.Position }, - new() { Type = ValueType.Int, Int = buyNow }, - new() { Type = 0, Int = 0 } - }; - addonShop->FireCallback(4, buyItem); - - _purchaseState.NextStep = DateTime.MaxValue; - _purchaseState.IsAwaitingYesNo = true; - } - } - else - { - _pluginLog.Information( - $"Stopping item purchase (desired = {_purchaseState.DesiredItems}, owned = {_purchaseState.OwnedItems})"); - _purchaseState = null; - _externalPluginHandler.Restore(); - } - } - - public void Dispose() - { - _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, "Shop", ShopPostSetup); - _addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "Shop", ShopPreFinalize); - _addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, "PostUpdate", ShopPostUpdate); - } - - private sealed class ItemForSale - { - public required int Position { get; init; } - public required uint ItemId { get; init; } - public required string? ItemName { get; init; } - public required uint Price { get; init; } - public required uint OwnedItems { get; init; } - } - - private sealed class PurchaseState - { - public PurchaseState(int desiredItems, int ownedItems) - { - DesiredItems = desiredItems; - OwnedItems = ownedItems; - } - - public int DesiredItems { get; } - public int OwnedItems { get; set; } - public int ItemsLeftToBuy => Math.Max(0, DesiredItems - OwnedItems); - public bool IsComplete => ItemsLeftToBuy == 0; - public bool IsAwaitingYesNo { get; set; } - public DateTime NextStep { get; set; } = DateTime.MinValue; + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.Int, Int = ItemForSale!.Position }, + new() { Type = ValueType.Int, Int = buyNow }, + new() { Type = 0, Int = 0 } + }; + addonShop->FireCallback(4, buyItem); } } diff --git a/Workshoppa/Windows/ShopWindow.cs b/Workshoppa/Windows/ShopWindow.cs new file mode 100644 index 0000000..1382325 --- /dev/null +++ b/Workshoppa/Windows/ShopWindow.cs @@ -0,0 +1,197 @@ +using System; +using System.Numerics; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin.Services; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Component.GUI; +using ImGuiNET; +using LLib.GameUI; +using Workshoppa.External; +using Workshoppa.GameData.Shops; + +namespace Workshoppa.Windows; + +internal abstract class ShopWindow : Window, IDisposable +{ + private readonly string _addonName; + private readonly WorkshopPlugin _plugin; + private readonly IPluginLog _pluginLog; + private readonly IGameGui _gameGui; + private readonly IAddonLifecycle _addonLifecycle; + private readonly ExternalPluginHandler _externalPluginHandler; + + protected ItemForSale? ItemForSale; + protected PurchaseState? PurchaseState; + + protected ShopWindow(string name, string addonName, WorkshopPlugin plugin, IPluginLog pluginLog, + IGameGui gameGui, IAddonLifecycle addonLifecycle, ExternalPluginHandler externalPluginHandler) + : base(name) + { + _addonName = addonName; + _plugin = plugin; + _pluginLog = pluginLog; + _gameGui = gameGui; + _addonLifecycle = addonLifecycle; + _externalPluginHandler = externalPluginHandler; + + Position = new Vector2(100, 100); + PositionCondition = ImGuiCond.Always; + Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoNav | ImGuiWindowFlags.NoCollapse; + + _addonLifecycle.RegisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup); + _addonLifecycle.RegisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize); + _addonLifecycle.RegisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate); + } + + public bool AutoBuyEnabled => PurchaseState != null; + + protected abstract bool Enabled { get; } + + public bool IsAwaitingYesNo + { + get => PurchaseState?.IsAwaitingYesNo ?? false; + set => PurchaseState!.IsAwaitingYesNo = value; + } + + private unsafe void ShopPostSetup(AddonEvent type, AddonArgs args) + { + if (!Enabled) + { + ItemForSale = null; + IsOpen = false; + return; + } + + UpdateShopStock((AtkUnitBase*)args.Addon); + PostUpdateShopStock(); + if (ItemForSale != null) + IsOpen = true; + } + + private void ShopPreFinalize(AddonEvent type, AddonArgs args) + { + PurchaseState = null; + _externalPluginHandler.Restore(); + + IsOpen = false; + } + + private unsafe void ShopPostUpdate(AddonEvent type, AddonArgs args) + { + if (!Enabled) + { + ItemForSale = null; + IsOpen = false; + return; + } + + UpdateShopStock((AtkUnitBase*)args.Addon); + PostUpdateShopStock(); + if (ItemForSale != null) + { + AtkUnitBase* addon = (AtkUnitBase*)args.Addon; + short x = 0, y = 0; + addon->GetPosition(&x, &y); + + short width = 0, height = 0; + addon->GetSize(&width, &height, true); + x += width; + + if ((short)Position!.Value.X != x || (short)Position!.Value.Y != y) + Position = new Vector2(x, y); + + IsOpen = true; + } + else + IsOpen = false; + } + + protected abstract unsafe void UpdateShopStock(AtkUnitBase* addon); + + private void PostUpdateShopStock() + { + if (ItemForSale != null && PurchaseState != null) + { + int ownedItems = (int)ItemForSale.OwnedItems; + if (PurchaseState.OwnedItems != ownedItems) + { + PurchaseState.OwnedItems = ownedItems; + PurchaseState.NextStep = DateTime.Now.AddSeconds(0.25); + } + } + } + + protected unsafe int GetItemCount(uint itemId) + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + return inventoryManager->GetInventoryItemCount(itemId, checkEquipped: false, checkArmory: false); + } + + protected abstract int GetCurrencyCount(); + + protected int GetMaxItemsToPurchase() + { + if (ItemForSale == null) + return 0; + + int currency = GetCurrencyCount(); + return (int)(currency / ItemForSale!.Price); + } + + protected void CancelAutoPurchase() + { + PurchaseState = null; + _externalPluginHandler.Restore(); + } + + protected void StartAutoPurchase(int toPurchase) + { + PurchaseState = new((int)ItemForSale!.OwnedItems + toPurchase, (int)ItemForSale.OwnedItems); + _externalPluginHandler.Save(); + } + + protected unsafe void HandleNextPurchaseStep() + { + if (ItemForSale == null || PurchaseState == null) + return; + + if (!_plugin.HasFreeInventorySlot()) + { + _pluginLog.Warning($"No free inventory slots, can't buy more {ItemForSale.ItemName}"); + PurchaseState = null; + _externalPluginHandler.Restore(); + } + else if (!PurchaseState.IsComplete) + { + if (PurchaseState.NextStep <= DateTime.Now && + _gameGui.TryGetAddonByName(_addonName, out AtkUnitBase* addonShop)) + { + int buyNow = Math.Min(PurchaseState.ItemsLeftToBuy, 99); + _pluginLog.Information($"Buying {buyNow}x {ItemForSale.ItemName}"); + + FirePurchaseCallback(addonShop, buyNow); + + PurchaseState.NextStep = DateTime.MaxValue; + PurchaseState.IsAwaitingYesNo = true; + } + } + else + { + _pluginLog.Information( + $"Stopping item purchase (desired = {PurchaseState.DesiredItems}, owned = {PurchaseState.OwnedItems})"); + PurchaseState = null; + _externalPluginHandler.Restore(); + } + } + + protected abstract unsafe void FirePurchaseCallback(AtkUnitBase* addonShop, int buyNow); + + public void Dispose() + { + _addonLifecycle.UnregisterListener(AddonEvent.PostSetup, _addonName, ShopPostSetup); + _addonLifecycle.UnregisterListener(AddonEvent.PreFinalize, _addonName, ShopPreFinalize); + _addonLifecycle.UnregisterListener(AddonEvent.PostUpdate, _addonName, ShopPostUpdate); + } +} diff --git a/Workshoppa/WorkshopPlugin.GameFunctions.cs b/Workshoppa/WorkshopPlugin.GameFunctions.cs index 50469f9..9e45111 100644 --- a/Workshoppa/WorkshopPlugin.GameFunctions.cs +++ b/Workshoppa/WorkshopPlugin.GameFunctions.cs @@ -222,12 +222,15 @@ partial class WorkshopPlugin return false; } - public unsafe bool HasFreeInventorySlot() + public bool HasFreeInventorySlot() => GetFreeInventorySlots() > 0; + + public unsafe int GetFreeInventorySlots() { var inventoryManger = InventoryManager.Instance(); if (inventoryManger == null) - return false; + return 0; + int count = 0; for (InventoryType t = InventoryType.Inventory1; t <= InventoryType.Inventory4; ++t) { var container = inventoryManger->GetInventoryContainer(t); @@ -235,10 +238,10 @@ partial class WorkshopPlugin { var item = container->GetInventorySlot(i); if (item == null || item->ItemID == 0) - return true; + ++count; } } - return false; + return count; } } diff --git a/Workshoppa/WorkshopPlugin.SelectYesNo.cs b/Workshoppa/WorkshopPlugin.SelectYesNo.cs index e6a907c..db33c52 100644 --- a/Workshoppa/WorkshopPlugin.SelectYesNo.cs +++ b/Workshoppa/WorkshopPlugin.SelectYesNo.cs @@ -19,7 +19,7 @@ partial class WorkshopPlugin if (_repairKitWindow.IsOpen) { _pluginLog.Verbose($"Checking for Repair Kit YesNo ({_repairKitWindow.AutoBuyEnabled}, {_repairKitWindow.IsAwaitingYesNo})"); - if (_repairKitWindow.AutoBuyEnabled && _repairKitWindow.IsAwaitingYesNo && _gameStrings.PurchaseItem.IsMatch(text)) + if (_repairKitWindow.AutoBuyEnabled && _repairKitWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForGil.IsMatch(text)) { _pluginLog.Information($"Selecting 'yes' ({text})"); _repairKitWindow.IsAwaitingYesNo = false; @@ -30,6 +30,20 @@ partial class WorkshopPlugin _pluginLog.Verbose("Not a purchase confirmation match"); } } + else if (_ceruleumTankWindow.IsOpen) + { + _pluginLog.Verbose($"Checking for Ceruleum Tank YesNo ({_ceruleumTankWindow.AutoBuyEnabled}, {_ceruleumTankWindow.IsAwaitingYesNo})"); + if (_ceruleumTankWindow.AutoBuyEnabled && _ceruleumTankWindow.IsAwaitingYesNo && _gameStrings.PurchaseItemForCompanyCredits.IsMatch(text)) + { + _pluginLog.Information($"Selecting 'yes' ({text})"); + _ceruleumTankWindow.IsAwaitingYesNo = false; + addonSelectYesNo->AtkUnitBase.FireCallbackInt(0); + } + else + { + _pluginLog.Verbose("Not a purchase confirmation match"); + } + } else if (CurrentStage != Stage.Stopped) { if (CurrentStage == Stage.ConfirmMaterialDelivery && _gameStrings.TurnInHighQualityItem == text) diff --git a/Workshoppa/WorkshopPlugin.cs b/Workshoppa/WorkshopPlugin.cs index 7ffdbec..0b9a832 100644 --- a/Workshoppa/WorkshopPlugin.cs +++ b/Workshoppa/WorkshopPlugin.cs @@ -8,7 +8,6 @@ using Dalamud.Game.Command; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; -using LLib; using Workshoppa.External; using Workshoppa.GameData; using Workshoppa.Windows; @@ -18,7 +17,7 @@ namespace Workshoppa; [SuppressMessage("ReSharper", "UnusedType.Global")] public sealed partial class WorkshopPlugin : IDalamudPlugin { - private readonly IReadOnlyList FabricationStationIds = new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly(); + private readonly IReadOnlyList _fabricationStationIds = new uint[] { 2005236, 2005238, 2005240, 2007821, 2011588 }.AsReadOnly(); internal readonly IReadOnlyList WorkshopTerritories = new ushort[] { 423, 424, 425, 653, 984 }.AsReadOnly(); private readonly WindowSystem _windowSystem = new WindowSystem(nameof(WorkshopPlugin)); @@ -40,6 +39,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin private readonly MainWindow _mainWindow; private readonly ConfigWindow _configWindow; private readonly RepairKitWindow _repairKitWindow; + private readonly CeruleumTankWindow _ceruleumTankWindow; private Stage _currentStageInternal = Stage.Stopped; private DateTime _continueAt = DateTime.MinValue; @@ -70,6 +70,9 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin _windowSystem.AddWindow(_configWindow); _repairKitWindow = new(this, _pluginInterface, _pluginLog, _gameGui, addonLifecycle, _configuration, _externalPluginHandler); _windowSystem.AddWindow(_repairKitWindow); + _ceruleumTankWindow = new(this, _pluginInterface, _pluginLog, _gameGui, addonLifecycle, _configuration, + _externalPluginHandler); + _windowSystem.AddWindow(_ceruleumTankWindow); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenMainUi += OpenMainUi; @@ -106,7 +109,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin _condition[ConditionFlag.BoundByDuty] || _condition[ConditionFlag.BetweenAreas] || _condition[ConditionFlag.BetweenAreas51] || - GetDistanceToEventObject(FabricationStationIds, out var fabricationStation) >= 3f) + GetDistanceToEventObject(_fabricationStationIds, out var fabricationStation) >= 3f) { _mainWindow.NearFabricationStation = false; @@ -255,6 +258,7 @@ public sealed partial class WorkshopPlugin : IDalamudPlugin _pluginInterface.UiBuilder.OpenMainUi -= OpenMainUi; _framework.Update -= FrameworkUpdate; + _ceruleumTankWindow.Dispose(); _repairKitWindow.Dispose(); _externalPluginHandler.RestoreTextAdvance(); diff --git a/Workshoppa/Workshoppa.csproj b/Workshoppa/Workshoppa.csproj index d2bff72..74dd627 100644 --- a/Workshoppa/Workshoppa.csproj +++ b/Workshoppa/Workshoppa.csproj @@ -1,7 +1,7 @@ net7.0-windows - 3.1 + 3.2 11.0 enable true