diff --git a/ARControl/ARControl.csproj b/ARControl/ARControl.csproj index 0e68239..688b18b 100644 --- a/ARControl/ARControl.csproj +++ b/ARControl/ARControl.csproj @@ -1,6 +1,6 @@ - 5.4 + 5.5 dist diff --git a/ARControl/AutoRetainerControlPlugin.cs b/ARControl/AutoRetainerControlPlugin.cs index 21a498e..0fb5507 100644 --- a/ARControl/AutoRetainerControlPlugin.cs +++ b/ARControl/AutoRetainerControlPlugin.cs @@ -63,7 +63,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin _allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog); _configWindow = new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache, - discardHelperIpc, _pluginLog); + discardHelperIpc, _allaganToolsIpc, _pluginLog); _windowSystem.AddWindow(_configWindow); ECommonsMain.Init(_pluginInterface, this); diff --git a/ARControl/External/AllaganToolsIpc.cs b/ARControl/External/AllaganToolsIpc.cs index 3aa4125..3896e43 100644 --- a/ARControl/External/AllaganToolsIpc.cs +++ b/ARControl/External/AllaganToolsIpc.cs @@ -1,4 +1,6 @@ -using System.Linq; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; using Dalamud.Plugin; using Dalamud.Plugin.Ipc; using Dalamud.Plugin.Ipc.Exceptions; @@ -24,14 +26,40 @@ internal sealed class AllaganToolsIpc } .Select(x => (uint)x).ToArray(); + private readonly ICallGateSubscriber> _getClownItems; private readonly ICallGateSubscriber _itemCountOwned; public AllaganToolsIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog) { _pluginLog = pluginLog; + _getClownItems = pluginInterface.GetIpcSubscriber>("AllaganTools.GetCharacterItems"); _itemCountOwned = pluginInterface.GetIpcSubscriber("AllaganTools.ItemCountOwned"); } + public List<(uint ItemId, uint Quantity)> GetCharacterItems(ulong contentId) + { + try + { + HashSet items = _getClownItems.InvokeFunc(contentId); + _pluginLog.Information($"CID: {contentId}, Items: {items.Count}"); + + return items.Select(x => (ItemId: (uint)x[2], Quantity: (uint)x[3])) + .GroupBy(x => x.ItemId) + .Select(x => (x.Key, (uint)x.Sum(y => y.Quantity))) + .ToList(); + } + catch (TargetInvocationException e) + { + _pluginLog.Information(e, $"Unable to retrieve items for character {contentId}"); + return []; + } + catch (IpcError) + { + _pluginLog.Warning("Could not query allagantools for character items"); + return []; + } + } + public uint GetRetainerItemCount(uint itemId) { try diff --git a/ARControl/Windows/Config/InventoryTab.cs b/ARControl/Windows/Config/InventoryTab.cs new file mode 100644 index 0000000..399079b --- /dev/null +++ b/ARControl/Windows/Config/InventoryTab.cs @@ -0,0 +1,282 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using ARControl.External; +using ARControl.GameData; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin.Services; +using ImGuiNET; + +namespace ARControl.Windows.Config; + +internal sealed class InventoryTab : ITab +{ + private readonly Configuration _configuration; + private readonly AllaganToolsIpc _allaganToolsIpc; + private readonly GameCache _gameCache; + private readonly IPluginLog _pluginLog; + + private List? _listAsTrees; + + public InventoryTab(Configuration configuration, AllaganToolsIpc allaganToolsIpc, GameCache gameCache, + IPluginLog pluginLog) + { + _configuration = configuration; + _allaganToolsIpc = allaganToolsIpc; + _gameCache = gameCache; + _pluginLog = pluginLog; + } + + public void Draw() + { + using var tab = ImRaii.TabItem("Inventory###TabInventory"); + if (!tab) + { + _listAsTrees = null; + return; + } + + if (_listAsTrees == null) + RefreshInventory(); + + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Redo, "Refresh")) + RefreshInventory(); + + ImGui.Separator(); + + if (_listAsTrees == null || _listAsTrees.Count == 0) + { + ImGui.Text("No items in inventory. Do you have AllaganTools installed?"); + return; + } + + foreach (var list in _configuration.ItemLists) + { + using var id = ImRaii.PushId($"List{list.Id}"); + if (ImGui.CollapsingHeader($"{list.Name} {list.GetIcon()}")) + { + using var indent = ImRaii.PushIndent(); + var rootNode = _listAsTrees.FirstOrDefault(x => x.Id == list.Id.ToString()); + if (rootNode == null || rootNode.Children.Count == 0) + { + ImGui.Text("This list is empty."); + continue; + } + + using var table = ImRaii.Table($"InventoryTable{list.Id}", 2, ImGuiTableFlags.NoSavedSettings); + if (!table) + continue; + + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.NoHide); + ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale); + + foreach (var child in rootNode.Children) + child.Draw(); + } + } + } + + private void RefreshInventory() + { + try + { + List inventories = new(); + foreach (Configuration.CharacterConfiguration character in _configuration.Characters) + { + List itemListIds = new(); + if (character.Type == Configuration.CharacterType.Standalone) + { + itemListIds = character.ItemListIds; + } + else if (character.Type == Configuration.CharacterType.PartOfCharacterGroup) + { + var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == character.CharacterGroupId); + if (group != null) + itemListIds = group.ItemListIds; + } + + var inventory = new CharacterInventory(character, itemListIds); + + var itemIdsOnLists = itemListIds.Where(listId => listId != Guid.Empty) + .Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId)) + .Where(list => list != null) + .SelectMany(list => list!.Items.Select(x => x.ItemId)) + .Distinct() + .ToHashSet(); + + UpdateOwnedItems(character.LocalContentId, inventory.Items, itemIdsOnLists); + foreach (var retainer in inventory.Retainers) + UpdateOwnedItems(retainer.Configuration.RetainerContentId, retainer.Items, itemIdsOnLists); + + inventories.Add(inventory); + } + + List listAsTrees = []; + if (inventories.Count > 0) + { + foreach (var list in _configuration.ItemLists) + { + TreeNode rootNode = new TreeNode(list.Id.ToString(), string.Empty, -1); + listAsTrees.Add(rootNode); + + var relevantCharacters = inventories.Where(x => x.ItemListIds.Contains(list.Id)).ToList(); + foreach (var item in list.Items) + { + var venture = _gameCache.Ventures.FirstOrDefault(x => x.ItemId == item.ItemId); + var total = relevantCharacters.Sum(x => x.CountItems(item.ItemId, list.CheckRetainerInventory)); + TreeNode itemNode = rootNode.AddChild(item.InternalId.ToString(), venture?.Name ?? string.Empty, + total); + + foreach (var character in relevantCharacters) + { + string characterName = + $"{character.Configuration.CharacterName} @ {character.Configuration.WorldName}"; + long? stockQuantity = list.Type == Configuration.ListType.KeepStocked + ? item.RemainingQuantity + : null; + uint characterCount = character.CountItems(item.ItemId, list.CheckRetainerInventory); + if (characterCount == 0) + continue; + var characterNode = itemNode.AddChild( + character.Configuration.LocalContentId.ToString(CultureInfo.InvariantCulture), + characterName, characterCount, stockQuantity); + + if (list.CheckRetainerInventory) + { + characterNode.AddChild("Self", "In Inventory", + character.CountItems(item.ItemId, false)); + + foreach (var retainer in character.Retainers) + { + uint retainerCount = retainer.CountItems(item.ItemId); + if (retainerCount == 0) + continue; + characterNode.AddChild( + retainer.Configuration.RetainerContentId.ToString(CultureInfo.InvariantCulture), + retainer.Configuration.Name, retainerCount); + } + } + } + } + } + } + + _listAsTrees = listAsTrees; + } + catch (Exception e) + { + _listAsTrees = []; + _pluginLog.Error(e, "Failed to load inventories via AllaganTools"); + } + } + + private void UpdateOwnedItems(ulong localContentId, List items, HashSet itemIdsOnLists) + { + var ownedItems = _allaganToolsIpc.GetCharacterItems(localContentId); + foreach (var ownedItem in ownedItems) + { + if (!itemIdsOnLists.Contains(ownedItem.ItemId)) + continue; + + items.Add(new Item(ownedItem.ItemId, ownedItem.Quantity)); + } + } + + private sealed class CharacterInventory + { + public CharacterInventory(Configuration.CharacterConfiguration configuration, List itemListIds) + { + Configuration = configuration; + ItemListIds = itemListIds; + Retainers = configuration.Retainers.Where(x => x is { Job: > 0, Managed: true }) + .OrderBy(x => x.DisplayOrder) + .ThenBy(x => x.RetainerContentId) + .Select(x => new RetainerInventory(x)) + .ToList(); + } + + public Configuration.CharacterConfiguration Configuration { get; } + public List ItemListIds { get; } + public List Retainers { get; } + public List Items { get; } = []; + + public uint CountItems(uint itemId, bool checkRetainerInventory) + { + uint sum = (uint)Items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity); + if (checkRetainerInventory) + sum += (uint)Retainers.Sum(x => x.CountItems(itemId)); + + return sum; + } + } + + private sealed class RetainerInventory(Configuration.RetainerConfiguration configuration) + { + public Configuration.RetainerConfiguration Configuration { get; } = configuration; + public List Items { get; } = []; + + public uint CountItems(uint itemId) => (uint)Items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity); + } + + private sealed record Item(uint ItemId, uint Quantity); + + private sealed record TreeNode(string Id, string Label, long Quantity, long? StockQuantity = null) + { + public List Children { get; } = []; + + public TreeNode AddChild(string id, string label, long quantity, long? stockQuantity = null) + { + TreeNode child = new TreeNode(id, label, quantity, stockQuantity); + Children.Add(child); + return child; + } + + public void Draw() + { + ImGui.TableNextRow(); + ImGui.TableNextColumn(); + if (Children.Count > 0) + { + bool open = ImGui.TreeNodeEx(Label, ImGuiTreeNodeFlags.SpanFullWidth); + + ImGui.TableNextColumn(); + DrawCount(); + + if (open) + { + foreach (var child in Children) + child.Draw(); + + ImGui.TreePop(); + } + } + else + { + ImGui.TreeNodeEx(Label, + ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth); + + ImGui.TableNextColumn(); + DrawCount(); + } + } + + private void DrawCount() + { + if (StockQuantity != null) + { + using var color = ImRaii.PushColor(ImGuiCol.Text, Quantity >= StockQuantity.Value + ? ImGuiColors.HealerGreen + : ImGuiColors.DalamudRed); + ImGui.TextUnformatted(string.Create(CultureInfo.CurrentCulture, + $"{Quantity:N0} / {StockQuantity.Value:N0}")); + } + else + ImGui.TextUnformatted(Quantity.ToString("N0", CultureInfo.CurrentCulture)); + } + } +} diff --git a/ARControl/Windows/Config/VentureListTab.cs b/ARControl/Windows/Config/VentureListTab.cs index 675d347..e8dbea8 100644 --- a/ARControl/Windows/Config/VentureListTab.cs +++ b/ARControl/Windows/Config/VentureListTab.cs @@ -456,18 +456,28 @@ internal sealed class VentureListTab : ITab })) { IDalamudTextureWrap? icon = _iconCache.GetIcon(filtered.Venture.IconId); + Vector2 pos = ImGui.GetCursorPos(); + Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y); + if (icon != null) { - ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); - ImGui.SameLine(); - ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); - - icon.Dispose(); + ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X, + ImGui.GetStyle().ItemSpacing.Y / 2)); } bool addThis = ImGui.Selectable( $"{filtered.Venture.Name} ({string.Join(" ", filtered.CategoryNames)})##SelectVenture{filtered.Venture.RowId}"); + + if (icon != null) + { + ImGui.SameLine(0, 0); + ImGui.SetCursorPos(pos); + ImGui.Image(icon.ImGuiHandle, iconSize); + + icon.Dispose(); + } + if (addThis || addFirst) { list.Items.Add(new Configuration.QueuedItem diff --git a/ARControl/Windows/ConfigWindow.cs b/ARControl/Windows/ConfigWindow.cs index 5ed7594..d85762d 100644 --- a/ARControl/Windows/ConfigWindow.cs +++ b/ARControl/Windows/ConfigWindow.cs @@ -41,6 +41,7 @@ internal sealed class ConfigWindow : LWindow ICommandManager commandManager, IconCache iconCache, DiscardHelperIpc discardHelperIpc, + AllaganToolsIpc allaganToolsIpc, IPluginLog pluginLog) : base($"ARC {SeIconChar.Collectible.ToIconString()}###ARControlConfig") { @@ -54,6 +55,7 @@ internal sealed class ConfigWindow : LWindow new VentureListTab(this, _configuration, gameCache, iconCache, discardHelperIpc, pluginLog), new CharacterGroupTab(this, _configuration), new RetainersTab(this, _configuration, iconCache), + new InventoryTab(_configuration, allaganToolsIpc, _gameCache, pluginLog), new LockedItemsTab(this, _configuration, clientState, commandManager, gameCache), new MiscTab(this, _configuration), ];