Option to buy multiple items

This commit is contained in:
Liza 2023-09-24 12:14:43 +02:00
parent db93d36c06
commit 601c928f18
Signed by: liza
GPG Key ID: 7199F8D727D55F67
9 changed files with 561 additions and 353 deletions

View File

@ -8,10 +8,17 @@ internal sealed class Configuration : IPluginConfiguration
public int Version { get; set; } = 1; public int Version { get; set; } = 1;
public List<uint> ItemsAvailableForPurchase { get; set; } = new(); public List<uint> ItemsAvailableForPurchase { get; set; } = new();
public uint SelectedPurchaseItemId { get; set; } = 0; public List<PurchasePriority> ItemsToPurchase { get; set; } = new();
public int ReservedSealCount { get; set; } = 0; public int ReservedSealCount { get; set; } = 0;
public ItemFilterType ItemFilter { get; set; } = ItemFilterType.HideGearSetItems; 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 public enum ItemFilterType
{ {

View File

@ -1,6 +1,7 @@
using System; using System;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
@ -23,16 +24,38 @@ partial class DeliverooPlugin
CurrentStage = Stage.SelectRewardTier; 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() private unsafe void SelectRewardTier()
{ {
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
return;
}
if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) && if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
IsAddonReady(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[] var selectRank = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 1 }, 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 }, 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() private unsafe void SelectRewardSubCategory()
{ {
PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{
CurrentStage = Stage.CloseGcExchange;
return;
}
if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) && if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var addonExchange) &&
IsAddonReady(addonExchange)) IsAddonReady(addonExchange))
{ {
PluginLog.Information($"Selecting subcategory 2, {(int)_selectedRewardItem.SubCategory}"); PluginLog.Information($"Selecting subcategory 2, {(int)item.SubCategory}");
var selectType = stackalloc AtkValue[] var selectType = stackalloc AtkValue[]
{ {
new() { Type = ValueType.Int, Int = 2 }, 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 }, 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)) if (SelectRewardItem(addonExchange))
{ {
_continueAt = DateTime.Now.AddSeconds(0.5); _continueAt = DateTime.Now.AddSeconds(0.2);
CurrentStage = Stage.ConfirmReward; CurrentStage = Stage.ConfirmReward;
} }
else else
{ {
PluginLog.Warning("Could not find selected reward item"); _continueAt = DateTime.Now.AddSeconds(0.2);
_continueAt = DateTime.Now.AddSeconds(0.5);
CurrentStage = Stage.CloseGcExchange; 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() private void ConfirmReward()
{ {
if (SelectSelectYesno(0, s => s.StartsWith("Exchange "))) PurchaseItemRequest? item = GetNextItemToPurchase();
if (item == null)
{ {
CurrentStage = Stage.CloseGcExchange; 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); _continueAt = DateTime.Now.AddSeconds(0.5);
} }
} }

View File

@ -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<TurnInItem> BuildTurnInList(AgentGrandCompanySupply* agent)
{
List<TurnInItem> 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] <Item's Column 19 in the sheet, but that has no name>
// 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<T>(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<AddonSelectString>("SelectString", out var addonSelectString) &&
IsAddonReady(&addonSelectString->AtkUnitBase))
{
addonSelectString->AtkUnitBase.FireCallbackInt(choice);
return true;
}
return false;
}
private unsafe bool SelectSelectYesno(int choice, Predicate<string> predicate)
{
if (TryGetAddonByName<AddonSelectYesno>("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;
}
}

View File

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Game.ClientState.Objects.Types;
using Dalamud.Logging; using Dalamud.Logging;
using Dalamud.Memory;
using Deliveroo.GameData; using Deliveroo.GameData;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -92,6 +92,7 @@ partial class DeliverooPlugin
var agent = (AgentGrandCompanySupply*)agentInterface; var agent = (AgentGrandCompanySupply*)agentInterface;
List<TurnInItem> items = BuildTurnInList(agent); List<TurnInItem> items = BuildTurnInList(agent);
_turnInWindow.EstimatedGcSeals = GetCurrentSealCount() + items.Sum(x => x.SealsWithBonus);
if (items.Count == 0 || addon->UldManager.NodeList[20]->IsVisible) if (items.Count == 0 || addon->UldManager.NodeList[20]->IsVisible)
{ {
CurrentStage = Stage.CloseGcSupplyThenStop; CurrentStage = Stage.CloseGcSupplyThenStop;
@ -151,7 +152,7 @@ partial class DeliverooPlugin
{ {
if (SelectSelectString(3)) if (SelectSelectString(3))
{ {
if (!_selectedRewardItem.IsValid()) if (GetNextItemToPurchase() == null)
{ {
_turnInWindow.State = false; _turnInWindow.State = false;
CurrentStage = Stage.RequestStop; CurrentStage = Stage.RequestStop;
@ -169,13 +170,13 @@ partial class DeliverooPlugin
{ {
if (SelectSelectString(3)) if (SelectSelectString(3))
{ {
if (!_selectedRewardItem.IsValid()) if (GetNextItemToPurchase() == null)
{ {
_turnInWindow.State = false; _turnInWindow.State = false;
CurrentStage = Stage.RequestStop; CurrentStage = Stage.RequestStop;
} }
else if (GetCurrentSealCount() <= else if (GetCurrentSealCount() <=
_configuration.ReservedSealCount + _selectedRewardItem.SealCost) _configuration.ReservedSealCount + GetNextItemToPurchase()!.SealCost)
{ {
_turnInWindow.State = false; _turnInWindow.State = false;
CurrentStage = Stage.RequestStop; CurrentStage = Stage.RequestStop;

View File

@ -57,7 +57,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
private Stage _currentStageInternal = Stage.Stopped; private Stage _currentStageInternal = Stage.Stopped;
private DateTime _continueAt = DateTime.MinValue; private DateTime _continueAt = DateTime.MinValue;
private GcRewardItem _selectedRewardItem = GcRewardItem.None; private List<PurchaseItemRequest> _itemsToPurchaseNow = new();
private (bool Saved, bool? PreviousState) _yesAlreadyState = (false, null); private (bool Saved, bool? PreviousState) _yesAlreadyState = (false, null);
public DeliverooPlugin(DalamudPluginInterface pluginInterface, ChatGui chatGui, GameGui gameGui, 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) else if (_turnInWindow.State && CurrentStage == Stage.Stopped)
{ {
CurrentStage = Stage.TargetPersonnelOfficer; CurrentStage = Stage.TargetPersonnelOfficer;
_selectedRewardItem = _turnInWindow.SelectedItem; _itemsToPurchaseNow = _turnInWindow.SelectedItems;
if (_selectedRewardItem.IsValid() && _selectedRewardItem.RequiredRank > GetGrandCompanyRank()) if (_itemsToPurchaseNow.Count > 0)
_selectedRewardItem = GcRewardItem.None; {
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; CurrentStage = Stage.TargetQuartermaster;
if (TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", out var gcSupplyList) && if (TryGetAddonByName<AddonGrandCompanySupplyList>("GrandCompanySupplyList", out var gcSupplyList) &&
@ -150,7 +158,7 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) && if (TryGetAddonByName<AtkUnitBase>("GrandCompanyExchange", out var gcExchange) &&
IsAddonReady(gcExchange)) IsAddonReady(gcExchange))
CurrentStage = Stage.CloseGcExchange; CurrentStage = Stage.SelectRewardTier;
} }
if (CurrentStage != Stage.Stopped && CurrentStage != Stage.RequestStop && !_yesAlreadyState.Saved) 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() public void Dispose()
{ {
@ -260,232 +246,6 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
RestoreYesAlready(); RestoreYesAlready();
} }
private unsafe List<TurnInItem> BuildTurnInList(AgentGrandCompanySupply* agent)
{
List<TurnInItem> 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] <Item's Column 19 in the sheet, but that has no name>
// 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<T>(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<AddonSelectString>("SelectString", out var addonSelectString) &&
IsAddonReady(&addonSelectString->AtkUnitBase))
{
addonSelectString->AtkUnitBase.FireCallbackInt(choice);
return true;
}
return false;
}
private unsafe bool SelectSelectYesno(int choice, Predicate<string> predicate)
{
if (TryGetAddonByName<AddonSelectYesno>("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() private void SaveYesAlready()
{ {
if (_yesAlreadyState.Saved) if (_yesAlreadyState.Saved)
@ -509,26 +269,4 @@ public sealed partial class DeliverooPlugin : IDalamudPlugin
_yesAlreadyState = (false, null); _yesAlreadyState = (false, null);
} }
internal enum Stage
{
TargetPersonnelOfficer,
OpenGcSupply,
SelectExpertDeliveryTab,
SelectItemToTurnIn,
TurnInSelected,
FinalizeTurnIn,
CloseGcSupply,
CloseGcSupplyThenStop,
TargetQuartermaster,
SelectRewardTier,
SelectRewardSubCategory,
SelectReward,
ConfirmReward,
CloseGcExchange,
RequestStop,
Stopped,
}
} }

View File

@ -1,15 +1,12 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Linq; using System.Linq;
using Dalamud;
using Dalamud.Data; using Dalamud.Data;
using Dalamud.Logging;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany; using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Deliveroo.GameData; namespace Deliveroo.GameData;
internal class GcRewardsCache internal sealed class GcRewardsCache
{ {
public GcRewardsCache(DataManager dataManager) public GcRewardsCache(DataManager dataManager)
{ {
@ -17,7 +14,7 @@ internal class GcRewardsCache
.Where(x => x.RowId > 0) .Where(x => x.RowId > 0)
.ToDictionary(x => x.RowId, .ToDictionary(x => x.RowId,
x => x =>
(Gc: (GrandCompany)x.GrandCompany.Row, (GrandCompany: (GrandCompany)x.GrandCompany.Row,
Tier: (RewardTier)x.Tier, Tier: (RewardTier)x.Tier,
SubCategory: (RewardSubCategory)x.SubCategory)); SubCategory: (RewardSubCategory)x.SubCategory));
@ -28,11 +25,11 @@ internal class GcRewardsCache
foreach (var item in items) foreach (var item in items)
{ {
var category = categories[item.RowId]; var category = categories[item.RowId];
Rewards[category.Gc].Add(new GcRewardItem Rewards[category.GrandCompany].Add(new GcRewardItem
{ {
ItemId = item.Item.Row, ItemId = item.Item.Row,
Name = item.Item.Value!.Name.ToString(), Name = item.Item.Value!.Name.ToString(),
GrandCompany = category.Gc, GrandCompany = category.GrandCompany,
Tier = category.Tier, Tier = category.Tier,
SubCategory = category.SubCategory, SubCategory = category.SubCategory,
RequiredRank = item.RequiredGrandCompanyRank.Row, RequiredRank = item.RequiredGrandCompanyRank.Row,
@ -48,4 +45,7 @@ internal class GcRewardsCache
{ GrandCompany.TwinAdder, new() }, { GrandCompany.TwinAdder, new() },
{ GrandCompany.ImmortalFlames, new() } { GrandCompany.ImmortalFlames, new() }
}; };
public GcRewardItem GetReward(GrandCompany grandCompany, uint itemId)
=> Rewards[grandCompany].Single(x => x.ItemId == itemId);
} }

View File

@ -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; }
}

23
Deliveroo/Stage.cs Normal file
View File

@ -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,
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using Dalamud.Interface; using Dalamud.Interface;
@ -20,7 +21,6 @@ internal sealed class TurnInWindow : Window
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly GcRewardsCache _gcRewardsCache; private readonly GcRewardsCache _gcRewardsCache;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private int _selectedAutoBuyItem;
public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration, public TurnInWindow(DeliverooPlugin plugin, DalamudPluginInterface pluginInterface, Configuration configuration,
GcRewardsCache gcRewardsCache, ConfigWindow configWindow) GcRewardsCache gcRewardsCache, ConfigWindow configWindow)
@ -35,48 +35,49 @@ internal sealed class TurnInWindow : Window
Position = new Vector2(100, 100); Position = new Vector2(100, 100);
PositionCondition = ImGuiCond.FirstUseEver; PositionCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(330, 50),
MaximumSize = new Vector2(500, 999),
};
Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse; Flags = ImGuiWindowFlags.AlwaysAutoResize | ImGuiWindowFlags.NoCollapse;
ShowCloseButton = false; ShowCloseButton = false;
} }
public bool State { get; set; } public bool State { get; set; }
public decimal Multiplier { private get; set; } public decimal Multiplier { private get; set; }
public int EstimatedGcSeals { private get; set; }
public string Error { private get; set; } = string.Empty; public string Error { private get; set; } = string.Empty;
private uint SelectedItemId public List<PurchaseItemRequest> SelectedItems
{ {
get get
{ {
if (_selectedAutoBuyItem == 0 || _selectedAutoBuyItem > _configuration.ItemsAvailableForPurchase.Count) GrandCompany grandCompany = _plugin.GetGrandCompany();
return 0; if (grandCompany == GrandCompany.None)
return new List<PurchaseItemRequest>();
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() public override void Draw()
@ -110,7 +111,9 @@ internal sealed class TurnInWindow : Window
if (!string.IsNullOrEmpty(Error)) if (!string.IsNullOrEmpty(Error))
{ {
ImGui.TextColored(ImGuiColors.DalamudRed, Error); ImGui.TextColored(ImGuiColors.DalamudRed, Error);
} else { }
else
{
if (Multiplier == 1m) if (Multiplier == 1m)
{ {
ImGui.TextColored(ImGuiColors.DalamudYellow, "You do not have an active seal buff."); 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.TextColored(ImGuiColors.HealerGreen, $"Current Buff: {(Multiplier - 1m) * 100:N0}%%");
} }
ImGui.Spacing(); ImGui.Unindent(27);
ImGui.Separator();
ImGui.BeginDisabled(state); ImGui.BeginDisabled(state);
List<string> comboValues = new() { GcRewardItem.None.Name }; ImGui.Text("Items to buy:");
foreach (var itemId in _configuration.ItemsAvailableForPurchase) DrawItemsToBuy(grandCompany);
{
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.EndDisabled(); ImGui.EndDisabled();
} }
ImGui.Unindent(27);
ImGui.Separator(); ImGui.Separator();
ImGui.Text($"Debug (State): {_plugin.CurrentStage}"); 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(); List<(uint ItemId, string Name, uint Rank)> comboValues = new()
return inventoryManager->GetInventoryItemCount(itemId, false, false, false); { (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);
}
} }
} }