diff --git a/Deliveroo/Configuration.cs b/Deliveroo/Configuration.cs index 62aeb86..dfa70f9 100644 --- a/Deliveroo/Configuration.cs +++ b/Deliveroo/Configuration.cs @@ -8,10 +8,17 @@ internal sealed class Configuration : IPluginConfiguration public int Version { get; set; } = 1; public List ItemsAvailableForPurchase { get; set; } = new(); - public uint SelectedPurchaseItemId { get; set; } = 0; + public List ItemsToPurchase { get; set; } = new(); public int ReservedSealCount { get; set; } = 0; public ItemFilterType ItemFilter { get; set; } = ItemFilterType.HideGearSetItems; + public bool IgnoreCertainLimitations { get; set; } = false; + + internal sealed class PurchasePriority + { + public uint ItemId { get; set; } + public int Limit { get; set; } + } public enum ItemFilterType { diff --git a/Deliveroo/DeliverooPlugin.Exchange.cs b/Deliveroo/DeliverooPlugin.Exchange.cs index f66b658..50c6dda 100644 --- a/Deliveroo/DeliverooPlugin.Exchange.cs +++ b/Deliveroo/DeliverooPlugin.Exchange.cs @@ -1,6 +1,7 @@ using System; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; +using Deliveroo.GameData; using FFXIVClientStructs.FFXIV.Component.GUI; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; @@ -23,16 +24,38 @@ partial class DeliverooPlugin CurrentStage = Stage.SelectRewardTier; } + private PurchaseItemRequest? GetNextItemToPurchase(PurchaseItemRequest? previousRequest = null) + { + foreach (PurchaseItemRequest request in _itemsToPurchaseNow) + { + int offset = 0; + if (request == previousRequest) + offset = (int)request.StackSize; + + if (GetItemCount(request.ItemId) + offset < request.EffectiveLimit) + return request; + } + + return null; + } + private unsafe void SelectRewardTier() { + PurchaseItemRequest? item = GetNextItemToPurchase(); + if (item == null) + { + CurrentStage = Stage.CloseGcExchange; + return; + } + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && IsAddonReady(addonExchange)) { - PluginLog.Information($"Selecting tier 1, {(int)_selectedRewardItem.Tier - 1}"); + PluginLog.Information($"Selecting tier 1, {(int)item.Tier - 1}"); var selectRank = stackalloc AtkValue[] { new() { Type = ValueType.Int, Int = 1 }, - new() { Type = ValueType.Int, Int = (int)_selectedRewardItem.Tier - 1 }, + new() { Type = ValueType.Int, Int = (int)item.Tier - 1 }, new() { Type = 0, Int = 0 }, new() { Type = 0, Int = 0 }, new() { Type = 0, Int = 0 }, @@ -49,14 +72,21 @@ partial class DeliverooPlugin private unsafe void SelectRewardSubCategory() { + PurchaseItemRequest? item = GetNextItemToPurchase(); + if (item == null) + { + CurrentStage = Stage.CloseGcExchange; + return; + } + if (TryGetAddonByName("GrandCompanyExchange", out var addonExchange) && IsAddonReady(addonExchange)) { - PluginLog.Information($"Selecting subcategory 2, {(int)_selectedRewardItem.SubCategory}"); + PluginLog.Information($"Selecting subcategory 2, {(int)item.SubCategory}"); var selectType = stackalloc AtkValue[] { new() { Type = ValueType.Int, Int = 2 }, - new() { Type = ValueType.Int, Int = (int)_selectedRewardItem.SubCategory }, + new() { Type = ValueType.Int, Int = (int)item.SubCategory }, new() { Type = 0, Int = 0 }, new() { Type = 0, Int = 0 }, new() { Type = 0, Int = 0 }, @@ -78,23 +108,79 @@ partial class DeliverooPlugin { if (SelectRewardItem(addonExchange)) { - _continueAt = DateTime.Now.AddSeconds(0.5); + _continueAt = DateTime.Now.AddSeconds(0.2); CurrentStage = Stage.ConfirmReward; } else { - PluginLog.Warning("Could not find selected reward item"); - _continueAt = DateTime.Now.AddSeconds(0.5); + _continueAt = DateTime.Now.AddSeconds(0.2); CurrentStage = Stage.CloseGcExchange; } } } + private unsafe bool SelectRewardItem(AtkUnitBase* addonExchange) + { + PurchaseItemRequest? item = GetNextItemToPurchase(); + if (item == null) + return false; + + uint itemsOnCurrentPage = addonExchange->AtkValues[1].UInt; + for (uint i = 0; i < itemsOnCurrentPage; ++i) + { + uint itemId = addonExchange->AtkValues[317 + i].UInt; + if (itemId == item.ItemId) + { + PluginLog.Information($"Selecting item {itemId}, {i}"); + long toBuy = (GetCurrentSealCount() - _configuration.ReservedSealCount) / item.SealCost; + toBuy = Math.Min(toBuy, item.EffectiveLimit - GetItemCount(item.ItemId)); + + if (item.ItemId != ItemIds.Venture && !_configuration.IgnoreCertainLimitations) + toBuy = Math.Min(toBuy, 99); + + if (toBuy <= 0) + { + PluginLog.Information($"Items to buy = {toBuy}"); + return false; + } + + _chatGui.Print($"Buying {toBuy}x {item.Name}..."); + var selectReward = stackalloc AtkValue[] + { + new() { Type = ValueType.Int, Int = 0 }, + new() { Type = ValueType.Int, Int = (int)i }, + new() { Type = ValueType.Int, Int = (int)toBuy }, + new() { Type = 0, Int = 0 }, + new() { Type = ValueType.Bool, Byte = 1 }, + new() { Type = ValueType.Bool, Byte = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 }, + new() { Type = 0, Int = 0 } + }; + addonExchange->FireCallback(9, selectReward); + return true; + } + } + + PluginLog.Warning("Could not find selected reward item"); + return false; + } + private void ConfirmReward() { - if (SelectSelectYesno(0, s => s.StartsWith("Exchange "))) + PurchaseItemRequest? item = GetNextItemToPurchase(); + if (item == null) { CurrentStage = Stage.CloseGcExchange; + return; + } + + if (SelectSelectYesno(0, s => s.StartsWith("Exchange "))) + { + if (GetNextItemToPurchase(item) != null) + CurrentStage = Stage.SelectRewardTier; + else + CurrentStage = Stage.CloseGcExchange; _continueAt = DateTime.Now.AddSeconds(0.5); } } diff --git a/Deliveroo/DeliverooPlugin.GameFunctions.cs b/Deliveroo/DeliverooPlugin.GameFunctions.cs new file mode 100644 index 0000000..161ee1a --- /dev/null +++ b/Deliveroo/DeliverooPlugin.GameFunctions.cs @@ -0,0 +1,228 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using Dalamud.Game.ClientState.Objects.Enums; +using Dalamud.Game.ClientState.Objects.Types; +using Dalamud.Logging; +using Dalamud.Memory; +using Deliveroo.GameData; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.Game.Control; +using FFXIVClientStructs.FFXIV.Client.Game.UI; +using FFXIVClientStructs.FFXIV.Client.UI; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Common.Math; +using FFXIVClientStructs.FFXIV.Component.GUI; + +namespace Deliveroo; + +partial class DeliverooPlugin +{ + private unsafe void InteractWithTarget(GameObject obj) + { + PluginLog.Information($"Setting target to {obj}"); + if (_targetManager.Target == null || _targetManager.Target != obj) + { + _targetManager.Target = obj; + } + + TargetSystem.Instance()->InteractWithObject( + (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false); + } + + private unsafe int GetCurrentSealCount() + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + switch ((GrandCompany)PlayerState.Instance()->GrandCompany) + { + case GrandCompany.Maelstrom: + return inventoryManager->GetInventoryItemCount(20, false, false, false); + case GrandCompany.TwinAdder: + return inventoryManager->GetInventoryItemCount(21, false, false, false); + case GrandCompany.ImmortalFlames: + return inventoryManager->GetInventoryItemCount(22, false, false, false); + default: + return 0; + } + } + + internal unsafe GrandCompany GetGrandCompany() => (GrandCompany)PlayerState.Instance()->GrandCompany; + + internal unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank(); + + private float GetDistanceToNpc(int npcId, out GameObject? o) + { + foreach (var obj in _objectTable) + { + if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c) + { + if (GetNpcId(obj) == npcId) + { + o = obj; + return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position); + } + } + } + + o = null; + return float.MaxValue; + } + + private int GetNpcId(GameObject obj) + { + return Marshal.ReadInt32(obj.Address + 128); + } + + private int GetPersonnelOfficerId() + { + return GetGrandCompany() switch + { + GrandCompany.Maelstrom => 0xF4B94, + GrandCompany.ImmortalFlames => 0xF4B97, + GrandCompany.TwinAdder => 0xF4B9A, + _ => int.MaxValue, + }; + } + + private int GetQuartermasterId() + { + return GetGrandCompany() switch + { + GrandCompany.Maelstrom => 0xF4B93, + GrandCompany.ImmortalFlames => 0xF4B96, + GrandCompany.TwinAdder => 0xF4B99, + _ => int.MaxValue, + }; + } + + private uint GetSealCap() => _sealCaps.TryGetValue(GetGrandCompanyRank(), out var cap) ? cap : 0; + + public unsafe int GetItemCount(uint itemId) + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + return inventoryManager->GetInventoryItemCount(itemId, false, false, false); + } + + private decimal GetSealMultiplier() + { + // priority seal allowance + if (_clientState.LocalPlayer!.StatusList.Any(x => x.StatusId == 1078)) + return 1.15m; + + // seal sweetener 1/2 + var fcStatus = _clientState.LocalPlayer!.StatusList.FirstOrDefault(x => x.StatusId == 414); + if (fcStatus != null) + { + return 1m + fcStatus.StackCount / 100m; + } + + return 1; + } + + private unsafe List BuildTurnInList(AgentGrandCompanySupply* agent) + { + List list = new(); + for (int i = 11 /* skip over provisioning items */; i < agent->NumItems; ++i) + { + GrandCompanyItem item = agent->ItemArray[i]; + + // this includes all items, even if they don't match the filter + list.Add(new TurnInItem + { + ItemId = Marshal.ReadInt32(new nint(&item) + 132), + Name = MemoryHelper.ReadSeString(&item.ItemName).ToString(), + SealsWithBonus = (int)Math.Round(item.SealReward * GetSealMultiplier(), MidpointRounding.AwayFromZero), + SealsWithoutBonus = item.SealReward, + ItemUiCategory = Marshal.ReadByte(new nint(&item) + 150), + }); + + // GrandCompanyItem + 104 = [int] InventoryType + // GrandCompanyItem + 108 = [int] ?? + // GrandCompanyItem + 124 = [int] + // GrandCompanyItem + 132 = [int] itemId + // GrandCompanyItem + 136 = [int] 0 (always)? + // GrandCompanyItem + 140 = [int] i (item's own position within the unsorted list) + // GrandCompanyItem + 148 = [short] ilvl + // GrandCompanyItem + 150 = [byte] ItemUICategory + // GrandCompanyItem + 151 = [byte] (unchecked) inventory slot in container + // GrandCompanyItem + 152 = [short] 512 (always)? + // int itemId = Marshal.ReadInt32(new nint(&item) + 132); + // PluginLog.Verbose($" {Marshal.ReadInt32(new nint(&item) + 132)};;;; {MemoryHelper.ReadSeString(&item.ItemName)}, {new nint(&agent->ItemArray[i]):X8}, {item.SealReward}, {item.IsTurnInAvailable}"); + } + + return list.OrderByDescending(x => x.SealsWithBonus) + .ThenBy(x => x.ItemUiCategory) + .ThenBy(x => x.ItemId) + .ToList(); + } + + private unsafe AtkUnitBase* GetAddonById(uint id) + { + var unitManagers = &AtkStage.GetSingleton()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; + for (var i = 0; i < 18; i++) + { + var unitManager = &unitManagers[i]; + var unitBaseArray = &(unitManager->AtkUnitEntries); + for (var j = 0; j < unitManager->Count; j++) + { + var unitBase = unitBaseArray[j]; + if (unitBase->ID == id) + { + return unitBase; + } + } + } + + return null; + } + + private unsafe bool TryGetAddonByName(string addonName, out T* addonPtr) + where T : unmanaged + { + var a = _gameGui.GetAddonByName(addonName); + if (a != IntPtr.Zero) + { + addonPtr = (T*)a; + return true; + } + else + { + addonPtr = null; + return false; + } + } + + private unsafe bool IsAddonReady(AtkUnitBase* addon) + { + return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; + } + + private unsafe bool SelectSelectString(int choice) + { + if (TryGetAddonByName("SelectString", out var addonSelectString) && + IsAddonReady(&addonSelectString->AtkUnitBase)) + { + addonSelectString->AtkUnitBase.FireCallbackInt(choice); + return true; + } + + return false; + } + + private unsafe bool SelectSelectYesno(int choice, Predicate predicate) + { + if (TryGetAddonByName("SelectYesno", out var addonSelectYesno) && + IsAddonReady(&addonSelectYesno->AtkUnitBase) && + predicate(MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString())) + { + PluginLog.Information( + $"Selecting choice={choice} for '{MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText)}'"); + + addonSelectYesno->AtkUnitBase.FireCallbackInt(choice); + return true; + } + + return false; + } +} diff --git a/Deliveroo/DeliverooPlugin.Supply.cs b/Deliveroo/DeliverooPlugin.Supply.cs index 3a4f8df..07ea99a 100644 --- a/Deliveroo/DeliverooPlugin.Supply.cs +++ b/Deliveroo/DeliverooPlugin.Supply.cs @@ -1,8 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Logging; -using Dalamud.Memory; using Deliveroo.GameData; using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI.Agent; @@ -92,6 +92,7 @@ partial class DeliverooPlugin var agent = (AgentGrandCompanySupply*)agentInterface; List items = BuildTurnInList(agent); + _turnInWindow.EstimatedGcSeals = GetCurrentSealCount() + items.Sum(x => x.SealsWithBonus); if (items.Count == 0 || addon->UldManager.NodeList[20]->IsVisible) { CurrentStage = Stage.CloseGcSupplyThenStop; @@ -151,7 +152,7 @@ partial class DeliverooPlugin { if (SelectSelectString(3)) { - if (!_selectedRewardItem.IsValid()) + if (GetNextItemToPurchase() == null) { _turnInWindow.State = false; CurrentStage = Stage.RequestStop; @@ -169,13 +170,13 @@ partial class DeliverooPlugin { if (SelectSelectString(3)) { - if (!_selectedRewardItem.IsValid()) + if (GetNextItemToPurchase() == null) { _turnInWindow.State = false; CurrentStage = Stage.RequestStop; } else if (GetCurrentSealCount() <= - _configuration.ReservedSealCount + _selectedRewardItem.SealCost) + _configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost) { _turnInWindow.State = false; CurrentStage = Stage.RequestStop; diff --git a/Deliveroo/DeliverooPlugin.cs b/Deliveroo/DeliverooPlugin.cs index 05e14d1..9e49b72 100644 --- a/Deliveroo/DeliverooPlugin.cs +++ b/Deliveroo/DeliverooPlugin.cs @@ -57,7 +57,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin private Stage _currentStageInternal = Stage.Stopped; private DateTime _continueAt = DateTime.MinValue; - private GcRewardItem _selectedRewardItem = GcRewardItem.None; + private List _itemsToPurchaseNow = new(); private (bool Saved, bool? PreviousState) _yesAlreadyState = (false, null); public DeliverooPlugin(DalamudPluginInterface pluginInterface, ChatGui chatGui, GameGui gameGui, @@ -137,11 +137,19 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin else if (_turnInWindow.State && CurrentStage == Stage.Stopped) { CurrentStage = Stage.TargetPersonnelOfficer; - _selectedRewardItem = _turnInWindow.SelectedItem; - if (_selectedRewardItem.IsValid() && _selectedRewardItem.RequiredRank > GetGrandCompanyRank()) - _selectedRewardItem = GcRewardItem.None; + _itemsToPurchaseNow = _turnInWindow.SelectedItems; + if (_itemsToPurchaseNow.Count > 0) + { + PluginLog.Information("Items to purchase:"); + foreach (var item in _itemsToPurchaseNow) + PluginLog.Information($" {item.Name} (limit = {item.EffectiveLimit})"); + } + else + PluginLog.Information("No items to purchase configured or available"); - if (_selectedRewardItem.IsValid() && GetCurrentSealCount() > GetSealCap() / 2) + + var nextItem = GetNextItemToPurchase(); + if (nextItem != null && GetCurrentSealCount() >= _configuration.ReservedSealCount + nextItem.SealCost) CurrentStage = Stage.TargetQuartermaster; if (TryGetAddonByName("GrandCompanySupplyList", out var gcSupplyList) && @@ -150,7 +158,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin if (TryGetAddonByName("GrandCompanyExchange", out var gcExchange) && IsAddonReady(gcExchange)) - CurrentStage = Stage.CloseGcExchange; + CurrentStage = Stage.SelectRewardTier; } if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_yesAlreadyState.Saved) @@ -228,28 +236,6 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin } } - private float GetDistanceToNpc(int npcId, out GameObject? o) - { - foreach (var obj in _objectTable) - { - if (obj.ObjectKind == ObjectKind.EventNpc && obj is Character c) - { - if (GetNpcId(obj) == npcId) - { - o = obj; - return Vector3.Distance(_clientState.LocalPlayer!.Position, c.Position); - } - } - } - - o = null; - return float.MaxValue; - } - - private int GetNpcId(GameObject obj) - { - return Marshal.ReadInt32(obj.Address + 128); - } public void Dispose() { @@ -260,232 +246,6 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin RestoreYesAlready(); } - private unsafe List BuildTurnInList(AgentGrandCompanySupply* agent) - { - List list = new(); - for (int i = 11 /* skip over provisioning items */; i < agent->NumItems; ++i) - { - GrandCompanyItem item = agent->ItemArray[i]; - - // this includes all items, even if they don't match the filter - list.Add(new TurnInItem - { - ItemId = Marshal.ReadInt32(new nint(&item) + 132), - Name = MemoryHelper.ReadSeString(&item.ItemName).ToString(), - SealsWithBonus = (int)Math.Round(item.SealReward * GetSealMultiplier(), MidpointRounding.AwayFromZero), - SealsWithoutBonus = item.SealReward, - ItemUiCategory = Marshal.ReadByte(new nint(&item) + 150), - }); - - // GrandCompanyItem + 104 = [int] InventoryType - // GrandCompanyItem + 108 = [int] ?? - // GrandCompanyItem + 124 = [int] - // GrandCompanyItem + 132 = [int] itemId - // GrandCompanyItem + 136 = [int] 0 (always)? - // GrandCompanyItem + 140 = [int] i (item's own position within the unsorted list) - // GrandCompanyItem + 148 = [short] ilvl - // GrandCompanyItem + 150 = [byte] ItemUICategory - // GrandCompanyItem + 151 = [byte] (unchecked) inventory slot in container - // GrandCompanyItem + 152 = [short] 512 (always)? - // int itemId = Marshal.ReadInt32(new nint(&item) + 132); - // PluginLog.Verbose($" {Marshal.ReadInt32(new nint(&item) + 132)};;;; {MemoryHelper.ReadSeString(&item.ItemName)}, {new nint(&agent->ItemArray[i]):X8}, {item.SealReward}, {item.IsTurnInAvailable}"); - } - - return list.OrderByDescending(x => x.SealsWithBonus) - .ThenBy(x => x.ItemUiCategory) - .ThenBy(x => x.ItemId) - .ToList(); - } - - private unsafe AtkUnitBase* GetAddonById(uint id) - { - var unitManagers = &AtkStage.GetSingleton()->RaptureAtkUnitManager->AtkUnitManager.DepthLayerOneList; - for (var i = 0; i < 18; i++) - { - var unitManager = &unitManagers[i]; - var unitBaseArray = &(unitManager->AtkUnitEntries); - for (var j = 0; j < unitManager->Count; j++) - { - var unitBase = unitBaseArray[j]; - if (unitBase->ID == id) - { - return unitBase; - } - } - } - - return null; - } - - private unsafe bool TryGetAddonByName(string addonName, out T* addonPtr) - where T : unmanaged - { - var a = _gameGui.GetAddonByName(addonName); - if (a != IntPtr.Zero) - { - addonPtr = (T*)a; - return true; - } - else - { - addonPtr = null; - return false; - } - } - - private unsafe bool IsAddonReady(AtkUnitBase* addon) - { - return addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded; - } - - private unsafe bool SelectRewardItem(AtkUnitBase* addonExchange) - { - uint itemsOnCurrentPage = addonExchange->AtkValues[1].UInt; - for (uint i = 0; i < itemsOnCurrentPage; ++i) - { - uint itemId = addonExchange->AtkValues[317 + i].UInt; - if (itemId == _selectedRewardItem.ItemId) - { - long toBuy = (GetCurrentSealCount() - _configuration.ReservedSealCount) / _selectedRewardItem.SealCost; - bool isVenture = _selectedRewardItem.ItemId == ItemIds.Venture; - if (isVenture) - toBuy = Math.Min(toBuy, 65000 - GetCurrentVentureCount()); - - if (toBuy == 0) - { - _turnInWindow.State = false; - CurrentStage = Stage.RequestStop; - break; - } - - PluginLog.Information($"Selecting item {itemId}, {i}"); - _chatGui.Print($"Buying {toBuy}x {_selectedRewardItem.Name}..."); - var selectReward = stackalloc AtkValue[] - { - new() { Type = ValueType.Int, Int = 0 }, - new() { Type = ValueType.Int, Int = (int)i }, - new() { Type = ValueType.Int, Int = (int)toBuy }, - new() { Type = 0, Int = 0 }, - new() { Type = ValueType.Bool, Byte = 1 }, - new() { Type = ValueType.Bool, Byte = 0 }, - new() { Type = 0, Int = 0 }, - new() { Type = 0, Int = 0 }, - new() { Type = 0, Int = 0 } - }; - addonExchange->FireCallback(9, selectReward); - return true; - } - } - - return false; - } - - private unsafe int GetCurrentSealCount() - { - InventoryManager* inventoryManager = InventoryManager.Instance(); - switch ((GrandCompany)PlayerState.Instance()->GrandCompany) - { - case GrandCompany.Maelstrom: - return inventoryManager->GetInventoryItemCount(20, false, false, false); - case GrandCompany.TwinAdder: - return inventoryManager->GetInventoryItemCount(21, false, false, false); - case GrandCompany.ImmortalFlames: - return inventoryManager->GetInventoryItemCount(22, false, false, false); - default: - return 0; - } - } - - internal unsafe GrandCompany GetGrandCompany() => (GrandCompany)PlayerState.Instance()->GrandCompany; - - internal unsafe byte GetGrandCompanyRank() => PlayerState.Instance()->GetGrandCompanyRank(); - - private int GetPersonnelOfficerId() - { - return GetGrandCompany() switch - { - GrandCompany.Maelstrom => 0xF4B94, - GrandCompany.ImmortalFlames => 0xF4B97, - GrandCompany.TwinAdder => 0xF4B9A, - _ => int.MaxValue, - }; - } - - private int GetQuartermasterId() - { - return GetGrandCompany() switch - { - GrandCompany.Maelstrom => 0xF4B93, - GrandCompany.ImmortalFlames => 0xF4B96, - GrandCompany.TwinAdder => 0xF4B99, - _ => int.MaxValue, - }; - } - - private uint GetSealCap() => _sealCaps.TryGetValue(GetGrandCompanyRank(), out var cap) ? cap : 0; - - private unsafe int GetCurrentVentureCount() - { - InventoryManager* inventoryManager = InventoryManager.Instance(); - return inventoryManager->GetInventoryItemCount(ItemIds.Venture, false, false, false); - } - - private unsafe bool SelectSelectString(int choice) - { - if (TryGetAddonByName("SelectString", out var addonSelectString) && - IsAddonReady(&addonSelectString->AtkUnitBase)) - { - addonSelectString->AtkUnitBase.FireCallbackInt(choice); - return true; - } - - return false; - } - - private unsafe bool SelectSelectYesno(int choice, Predicate predicate) - { - if (TryGetAddonByName("SelectYesno", out var addonSelectYesno) && - IsAddonReady(&addonSelectYesno->AtkUnitBase) && - predicate(MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText).ToString())) - { - PluginLog.Information( - $"Selecting choice={choice} for '{MemoryHelper.ReadSeString(&addonSelectYesno->PromptText->NodeText)}'"); - - addonSelectYesno->AtkUnitBase.FireCallbackInt(choice); - return true; - } - - return false; - } - - private decimal GetSealMultiplier() - { - // priority seal allowance - if (_clientState.LocalPlayer!.StatusList.Any(x => x.StatusId == 1078)) - return 1.15m; - - // seal sweetener 1/2 - var fcStatus = _clientState.LocalPlayer!.StatusList.FirstOrDefault(x => x.StatusId == 414); - if (fcStatus != null) - { - return 1m + fcStatus.StackCount / 100m; - } - - return 1; - } - - private unsafe void InteractWithTarget(GameObject obj) - { - PluginLog.Information($"Setting target to {obj}"); - if (_targetManager.Target == null || _targetManager.Target != obj) - { - _targetManager.Target = obj; - } - - TargetSystem.Instance()->InteractWithObject( - (FFXIVClientStructs.FFXIV.Client.Game.Object.GameObject*)obj.Address, false); - } - private void SaveYesAlready() { if (_yesAlreadyState.Saved) @@ -509,26 +269,4 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin _yesAlreadyState = (false, null); } - - internal enum Stage - { - TargetPersonnelOfficer, - OpenGcSupply, - SelectExpertDeliveryTab, - SelectItemToTurnIn, - TurnInSelected, - FinalizeTurnIn, - CloseGcSupply, - CloseGcSupplyThenStop, - - TargetQuartermaster, - SelectRewardTier, - SelectRewardSubCategory, - SelectReward, - ConfirmReward, - CloseGcExchange, - - RequestStop, - Stopped, - } } diff --git a/Deliveroo/GameData/GcRewardsCache.cs b/Deliveroo/GameData/GcRewardsCache.cs index e8ded0b..591ea0f 100644 --- a/Deliveroo/GameData/GcRewardsCache.cs +++ b/Deliveroo/GameData/GcRewardsCache.cs @@ -1,15 +1,12 @@ using System.Collections.Generic; -using System.Data.SqlTypes; using System.Linq; -using Dalamud; using Dalamud.Data; -using Dalamud.Logging; using Lumina.Excel.GeneratedSheets; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; namespace Deliveroo.GameData; -internal class GcRewardsCache +internal sealed class GcRewardsCache { public GcRewardsCache(DataManager dataManager) { @@ -17,7 +14,7 @@ internal class GcRewardsCache .Where(x => x.RowId > 0) .ToDictionary(x => x.RowId, x => - (Gc: (GrandCompany)x.GrandCompany.Row, + (GrandCompany: (GrandCompany)x.GrandCompany.Row, Tier: (RewardTier)x.Tier, SubCategory: (RewardSubCategory)x.SubCategory)); @@ -28,11 +25,11 @@ internal class GcRewardsCache foreach (var item in items) { var category = categories[item.RowId]; - Rewards[category.Gc].Add(new GcRewardItem + Rewards[category.GrandCompany].Add(new GcRewardItem { ItemId = item.Item.Row, Name = item.Item.Value!.Name.ToString(), - GrandCompany = category.Gc, + GrandCompany = category.GrandCompany, Tier = category.Tier, SubCategory = category.SubCategory, RequiredRank = item.RequiredGrandCompanyRank.Row, @@ -48,4 +45,7 @@ internal class GcRewardsCache { GrandCompany.TwinAdder, new() }, { GrandCompany.ImmortalFlames, new() } }; + + public GcRewardItem GetReward(GrandCompany grandCompany, uint itemId) + => Rewards[grandCompany].Single(x => x.ItemId == itemId); } diff --git a/Deliveroo/PurchaseItemRequest.cs b/Deliveroo/PurchaseItemRequest.cs new file mode 100644 index 0000000..5985578 --- /dev/null +++ b/Deliveroo/PurchaseItemRequest.cs @@ -0,0 +1,14 @@ +using Deliveroo.GameData; + +namespace Deliveroo; + +internal sealed class PurchaseItemRequest +{ + public required uint ItemId { get; init; } + public required string Name { get; set; } + public required uint EffectiveLimit { get; init; } + public required uint SealCost { get; init; } + public required RewardTier Tier { get; init; } + public required RewardSubCategory SubCategory { get; init; } + public required uint StackSize { get; init; } +} diff --git a/Deliveroo/Stage.cs b/Deliveroo/Stage.cs new file mode 100644 index 0000000..5fbe112 --- /dev/null +++ b/Deliveroo/Stage.cs @@ -0,0 +1,23 @@ +namespace Deliveroo; + +internal enum Stage +{ + TargetPersonnelOfficer, + OpenGcSupply, + SelectExpertDeliveryTab, + SelectItemToTurnIn, + TurnInSelected, + FinalizeTurnIn, + CloseGcSupply, + CloseGcSupplyThenStop, + + TargetQuartermaster, + SelectRewardTier, + SelectRewardSubCategory, + SelectReward, + ConfirmReward, + CloseGcExchange, + + RequestStop, + Stopped, +} diff --git a/Deliveroo/Windows/TurnInWindow.cs b/Deliveroo/Windows/TurnInWindow.cs index c141c5b..8563b32 100644 --- a/Deliveroo/Windows/TurnInWindow.cs +++ b/Deliveroo/Windows/TurnInWindow.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Interface; @@ -20,7 +21,6 @@ internal sealed class TurnInWindow : Window private readonly Configuration _configuration; private readonly GcRewardsCache _gcRewardsCache; private readonly ConfigWindow _configWindow; - private int _selectedAutoBuyItem; public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration, GcRewardsCache gcRewardsCache, ConfigWindow configWindow) @@ -35,48 +35,49 @@ internal sealed class TurnInWindow : Window Position = new Vector2(100, 100); PositionCondition = ImGuiCond.FirstUseEver; + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(330, 50), + MaximumSize = new Vector2(500, 999), + }; + Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse; ShowCloseButton = false; } public bool State { get; set; } public decimal Multiplier { private get; set; } + public int EstimatedGcSeals { private get; set; } public string Error { private get; set; } = string.Empty; - private uint SelectedItemId + public List SelectedItems { get { - if (_selectedAutoBuyItem == 0 || _selectedAutoBuyItem > _configuration.ItemsAvailableForPurchase.Count) - return 0; + GrandCompany grandCompany = _plugin.GetGrandCompany(); + if (grandCompany == GrandCompany.None) + return new List(); - return _configuration.ItemsAvailableForPurchase[_selectedAutoBuyItem - 1]; + var rank = _plugin.GetGrandCompanyRank(); + return _configuration.ItemsToPurchase + .Where(x => x.ItemId != 0) + .Select(x => new { Item = x, Reward = _gcRewardsCache.GetReward(grandCompany, x.ItemId) }) + .Where(x => x.Reward.RequiredRank <= rank) + .Select(x => new PurchaseItemRequest + { + ItemId = x.Item.ItemId, + Name = x.Reward.Name, + EffectiveLimit = CalculateEffectiveLimit( + x.Item.ItemId, + x.Item.Limit <= 0 ? uint.MaxValue : (uint)x.Item.Limit, + x.Reward.StackSize), + SealCost = x.Reward.SealCost, + Tier = x.Reward.Tier, + SubCategory = x.Reward.SubCategory, + StackSize = x.Reward.StackSize, + }) + .ToList(); } - set - { - int index = _configuration.ItemsAvailableForPurchase.IndexOf(value); - if (index >= 0) - _selectedAutoBuyItem = index + 1; - else - _selectedAutoBuyItem = 0; - } - } - - public GcRewardItem SelectedItem - { - get - { - uint selectedItemId = SelectedItemId; - if (selectedItemId == 0) - return GcRewardItem.None; - - return _gcRewardsCache.Rewards[_plugin.GetGrandCompany()].Single(x => x.ItemId == selectedItemId); - } - } - - public override void OnOpen() - { - SelectedItemId = _configuration.SelectedPurchaseItemId; } public override void Draw() @@ -110,7 +111,9 @@ internal sealed class TurnInWindow : Window if (!string.IsNullOrEmpty(Error)) { ImGui.TextColored(ImGuiColors.DalamudRed, Error); - } else { + } + else + { if (Multiplier == 1m) { ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff."); @@ -120,41 +123,149 @@ internal sealed class TurnInWindow : Window ImGui.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%"); } - ImGui.Spacing(); + ImGui.Unindent(27); + ImGui.Separator(); ImGui.BeginDisabled(state); - List comboValues = new() { GcRewardItem.None.Name }; - foreach (var itemId in _configuration.ItemsAvailableForPurchase) - { - var name = _gcRewardsCache.Rewards[grandCompany].First(x => x.ItemId == itemId).Name; - int itemCount = GetItemCount(itemId); - if (itemCount > 0) - comboValues.Add($"{name} ({itemCount:N0})"); - else - comboValues.Add(name); - } - - if (ImGui.Combo("", ref _selectedAutoBuyItem, comboValues.ToArray(), comboValues.Count)) - { - _configuration.SelectedPurchaseItemId = SelectedItemId; - _pluginInterface.SavePluginConfig(_configuration); - } - - if (SelectedItem.IsValid() && SelectedItem.RequiredRank > _plugin.GetGrandCompanyRank()) - ImGui.TextColored(ImGuiColors.DalamudRed, "Your rank isn't high enough to buy this item."); + ImGui.Text("Items to buy:"); + DrawItemsToBuy(grandCompany); ImGui.EndDisabled(); } - ImGui.Unindent(27); - ImGui.Separator(); ImGui.Text($"Debug (State): {_plugin.CurrentStage}"); + switch (_plugin.CurrentStage) + { + case Stage.SelectItemToTurnIn: + case Stage.TurnInSelected: + case Stage.FinalizeTurnIn: + case Stage.CloseGcSupply: + ImGui.Text($"Estimated Total Seal Count: {EstimatedGcSeals:N0}"); + break; + } } - private unsafe int GetItemCount(uint itemId) + private void DrawItemsToBuy(GrandCompany grandCompany) { - InventoryManager* inventoryManager = InventoryManager.Instance(); - return inventoryManager->GetInventoryItemCount(itemId, false, false, false); + List<(uint ItemId, string Name, uint Rank)> comboValues = new() + { (GcRewardItem.None.ItemId, GcRewardItem.None.Name, GcRewardItem.None.RequiredRank) }; + foreach (uint itemId in _configuration.ItemsAvailableForPurchase) + { + var gcReward = _gcRewardsCache.GetReward(grandCompany, itemId); + int itemCount = _plugin.GetItemCount(itemId); + if (itemCount > 0) + comboValues.Add((itemId, $"{gcReward.Name} ({itemCount:N0})", gcReward.RequiredRank)); + else + comboValues.Add((itemId, gcReward.Name, gcReward.RequiredRank)); + } + + if (_configuration.ItemsToPurchase.Count == 0) + _configuration.ItemsToPurchase.Add(new Configuration.PurchasePriority + { ItemId = GcRewardItem.None.ItemId, Limit = 0 }); + + int? itemToRemove = null; + for (int i = 0; i < _configuration.ItemsToPurchase.Count; ++i) + { + ImGui.PushID($"ItemToBuy{i}"); + var item = _configuration.ItemsToPurchase[i]; + int comboValueIndex = comboValues.FindIndex(x => x.ItemId == item.ItemId); + if (comboValueIndex < 0) + { + item.ItemId = 0; + item.Limit = 0; + _pluginInterface.SavePluginConfig(_configuration); + + comboValueIndex = 0; + } + + if (ImGui.Combo("", ref comboValueIndex, comboValues.Select(x => x.Name).ToArray(), comboValues.Count)) + { + item.ItemId = comboValues[comboValueIndex].ItemId; + _pluginInterface.SavePluginConfig(_configuration); + } + + if (_configuration.ItemsToPurchase.Count >= 2) + { + ImGui.SameLine(); + if (ImGuiComponents.IconButton($"###Remove{i}", FontAwesomeIcon.Times)) + itemToRemove = i; + } + + ImGui.Indent(27); + if (comboValueIndex > 0) + { + ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 130); + int limit = item.Limit; + if (item.ItemId == ItemIds.Venture) + limit = Math.Min(limit, 65_000); + + if (ImGui.InputInt("Maximum items to buy", ref limit, 50, 500)) + { + item.Limit = Math.Max(0, limit); + if (item.ItemId == ItemIds.Venture) + item.Limit = Math.Min(item.Limit, 65_000); + + _pluginInterface.SavePluginConfig(_configuration); + } + } + else if (item.Limit != 0) + { + item.Limit = 0; + _pluginInterface.SavePluginConfig(_configuration); + } + + if (comboValueIndex > 0 && comboValues[comboValueIndex].Rank > _plugin.GetGrandCompanyRank()) + { + ImGui.TextColored(ImGuiColors.DalamudRed, + "This item will be skipped, your rank isn't high enough to buy it."); + } + + ImGui.Unindent(27); + ImGui.PopID(); + } + + if (itemToRemove != null) + { + _configuration.ItemsToPurchase.RemoveAt(itemToRemove.Value); + _pluginInterface.SavePluginConfig(_configuration); + } + + if (_configuration.ItemsAvailableForPurchase.Any(x => _configuration.ItemsToPurchase.All(y => x != y.ItemId))) + { + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Item")) + { + _configuration.ItemsToPurchase.Add(new Configuration.PurchasePriority + { ItemId = GcRewardItem.None.ItemId, Limit = 0 }); + _pluginInterface.SavePluginConfig(_configuration); + } + } + } + + private unsafe uint CalculateEffectiveLimit(uint itemId, uint limit, uint stackSize) + { + if (itemId == ItemIds.Venture) + return Math.Min(limit, 65_000); + else + { + uint slotsThatCanBeUsed = 0; + InventoryManager* inventoryManager = InventoryManager.Instance(); + for (InventoryType inventoryType = InventoryType.Inventory1; + inventoryType <= InventoryType.Inventory4; + ++inventoryType) + { + var container = inventoryManager->GetInventoryContainer(inventoryType); + for (int i = 0; i < container->Size; ++i) + { + var item = container->GetInventorySlot(i); + if (item == null || item->ItemID == 0 || item->ItemID == itemId) + { + slotsThatCanBeUsed++; + } + } + } + + return Math.Min(limit, slotsThatCanBeUsed * stackSize); + } } }