diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user
index a250ab0..a860f90 100644
--- a/AetherBags.sln.DotSettings.user
+++ b/AetherBags.sln.DotSettings.user
@@ -1,2 +1,3 @@
- ForceIncluded
\ No newline at end of file
+ ForceIncluded
+ ForceIncluded
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs
index 4787062..de9ea4d 100644
--- a/AetherBags/Addons/AddonInventoryWindow.cs
+++ b/AetherBags/Addons/AddonInventoryWindow.cs
@@ -1,3 +1,5 @@
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AetherBags.Extensions;
@@ -15,58 +17,33 @@ namespace AetherBags.Addons;
public class AddonInventoryWindow : NativeAddon
{
- private InventoryCategoryNode _categoryNode;
- private InventoryDragDropNode _dragDropNode;
+ private WrappingGridNode _categoriesNode;
+
+ // Window constraints
+ private const float MinWindowWidth = 300;
+ private const float MaxWindowWidth = 800;
+ private const float MinWindowHeight = 200;
+ private const float MaxWindowHeight = 1000;
+
+ // Layout settings
+ private const float CategorySpacing = 10;
+ private const float ItemSize = 40;
+ private const float ItemPadding = 6;
+
protected override unsafe void OnSetup(AtkUnitBase* addon)
{
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
- _categoryNode = new InventoryCategoryNode
+ addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
+ _categoriesNode = new WrappingGridNode
{
Position = ContentStartPosition,
Size = ContentSize,
- Category = new CategoryInfo
- {
- Name = "AetherBags",
- },
- Items = InventoryState.GetInventoryItems()
+ HorizontalSpacing = CategorySpacing,
+ VerticalSpacing = CategorySpacing
};
- _categoryNode.AttachNode(this);
- /*
- var data = InventoryState.GetInventoryItems().Find(item => item.Name.Contains("Cookie"));
-
-
- if (data != null)
- {
- var item = data.Item;
- _dragDropNode = new InventoryDragDropNode
- {
- Size = new Vector2(48),
- IsVisible = true,
- IconId = data.IconId,
- AcceptedType = DragDropType.Nothing,
- IsDraggable = false,
- Payload = new DragDropPayload
- {
- Type = DragDropType.Item,
- Int1 = (int)data.Item.Container,
- Int2 = (int)data.Item.ItemId,
- },
- IsClickable = true,
- OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot),
- OnRollOut = node => node.HideTooltip(),
- OnClicked = _ =>
- {
-
- AgentInventoryContext* context = AgentInventoryContext.Instance();
- context->OpenForItemSlot(data.Item.Container, data.Item.Slot, 0, context->AddonId);
- //item.UseItem();
- },
- ItemInfo = data
- };
- _dragDropNode.AttachNode(this);
- }
- */
+ _categoriesNode.AttachNode(this);
+ RefreshCategories();
}
protected override unsafe void OnUpdate(AtkUnitBase* addon)
@@ -76,11 +53,94 @@ public class AddonInventoryWindow : NativeAddon
private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
{
+ RefreshCategories();
+ }
+ protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData) {
+ RefreshCategories();
+ }
+
+ private void RefreshCategories()
+ {
+ var categories = InventoryState.GetInventoryItemCategories();
+
+ float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
+ int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
+
+ _categoriesNode.SyncWithListData(
+ categories,
+ node => node.CategorizedInventory,
+ data =>
+ {
+ var node = new InventoryCategoryNode
+ {
+ Size = ContentSize with { Y = 120 },
+ CategorizedInventory = data
+ };
+
+ UpdateItemsPerLine(node, maxItemsPerLine);
+ return node;
+ });
+
+ foreach (InventoryCategoryNode node in _categoriesNode.GetNodes())
+ {
+ UpdateItemsPerLine(node, maxItemsPerLine);
+ }
+
+ AutoSizeWindow();
+ }
+
+ private static void UpdateItemsPerLine(InventoryCategoryNode node, int maxItemsPerLine)
+ {
+ int itemCount = node.CategorizedInventory.Items.Count;
+ int itemsPerLine = Math.Min(itemCount, maxItemsPerLine);
+ node.SetItemsPerLine(itemsPerLine);
+ }
+
+ private int CalculateOptimalItemsPerLine(float availableWidth)
+ {
+ float itemWithPadding = ItemSize + ItemPadding;
+ int maxItems = (int)Math.Floor((availableWidth + ItemPadding) / itemWithPadding);
+
+ return Math.Clamp(maxItems, 1, 15);
+ }
+
+ private void AutoSizeWindow()
+ {
+ List childNodes = _categoriesNode.GetNodes().ToList();
+ if (childNodes.Count == 0)
+ {
+ ResizeWindow(MinWindowWidth, MinWindowHeight);
+ return;
+ }
+
+ float requiredWidth = childNodes.Max(node => node. Width);
+ requiredWidth += ContentStartPosition.X * 2;
+ float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
+
+ float contentWidth = finalWidth - (ContentStartPosition.X * 2);
+ _categoriesNode.Size = new Vector2(contentWidth, MaxWindowHeight);
+
+ _categoriesNode.RecalculateLayout();
+
+ float requiredHeight = _categoriesNode.GetRequiredHeight();
+ requiredHeight += ContentStartPosition.Y + ContentStartPosition.X;
+
+ float finalHeight = Math.Clamp(requiredHeight, MinWindowHeight, MaxWindowHeight);
+
+ ResizeWindow(finalWidth, finalHeight);
+ }
+
+ private void ResizeWindow(float width, float height)
+ {
+ SetWindowSize(width, height);
+ _categoriesNode.Size = ContentSize;
+ _categoriesNode.RecalculateLayout();
}
protected override unsafe void OnFinalize(AtkUnitBase* addon)
{
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
+ addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
}
}
\ No newline at end of file
diff --git a/AetherBags/Extensions/AddonLifecycleExtensions.cs b/AetherBags/Extensions/AddonLifecycleExtensions.cs
new file mode 100644
index 0000000..03324f4
--- /dev/null
+++ b/AetherBags/Extensions/AddonLifecycleExtensions.cs
@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using System.Linq;
+using AetherBags;
+using Dalamud.Game.Addon.Lifecycle;
+using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Plugin.Services;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+public static class AddonLifecycleExtensions {
+ extension(IAddonLifecycle addonLifecycle) {
+ public void LogAddon(string addonName, params AddonEvent[] loggedModules) {
+ if (loggedModules.Length is 0) {
+ loggedModules = [
+ AddonEvent.PostSetup,
+ AddonEvent.PostOpen,
+ AddonEvent.PostClose,
+ AddonEvent.PostShow,
+ AddonEvent.PostHide,
+ AddonEvent.PostRefresh,
+ AddonEvent.PostRequestedUpdate,
+ AddonEvent.PreFinalize,
+ ];
+ }
+
+ ActiveLoggers.TryAdd(addonName, loggedModules.ToList());
+ foreach (var loggedModule in loggedModules) {
+ addonLifecycle.RegisterListener(loggedModule, addonName, Logger);
+ }
+ }
+
+ public void UnLogAddon(string addonName) {
+ if (!ActiveLoggers.TryGetValue(addonName, out var loggedModules)) return;
+
+ foreach (var loggedModule in loggedModules) {
+ addonLifecycle.UnregisterListener(loggedModule, addonName, Logger);
+
+ }
+ }
+ }
+
+ private static readonly Dictionary> ActiveLoggers = [];
+
+ private static void Logger(AddonEvent type, AddonArgs args) {
+ switch (args) {
+ case AddonReceiveEventArgs receiveEventArgs:
+ Services.Logger.Debug($"[{args.AddonName}] {(AtkEventType)receiveEventArgs.AtkEventType}: {receiveEventArgs.EventParam}");
+ break;
+
+ default:
+ Services.Logger.Debug($"{args.AddonName} called {type.ToString().Replace("Post", string.Empty)}");
+ break;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/CategorizedInventory.cs b/AetherBags/Inventory/CategorizedInventory.cs
new file mode 100644
index 0000000..d08e992
--- /dev/null
+++ b/AetherBags/Inventory/CategorizedInventory.cs
@@ -0,0 +1,5 @@
+using System.Collections.Generic;
+
+namespace AetherBags.Inventory;
+
+public readonly record struct CategorizedInventory(CategoryInfo Category, List Items);
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/InventoryState.cs
index f82b6c8..0b97f8d 100644
--- a/AetherBags/Inventory/InventoryState.cs
+++ b/AetherBags/Inventory/InventoryState.cs
@@ -3,6 +3,7 @@ using System.Linq;
using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using Lumina.Excel.Sheets;
namespace AetherBags.Inventory;
@@ -34,6 +35,22 @@ public static unsafe class InventoryState
public static bool Contains(this List inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type);
+ public static List GetInventoryItemCategories()
+ {
+ var items = GetInventoryItems();
+
+ return items
+ .GroupBy(GetItemUiCategoryKey)
+ .OrderBy(g => g.Key)
+ .Select(g =>
+ {
+ var category = GetCategoryInfoForKey(g.Key, g.FirstOrDefault());
+ var list = g.OrderByDescending(i => i.ItemCount).ToList();
+ return new CategorizedInventory(category, list);
+ })
+ .ToList();
+ }
+
public static List GetInventoryItems() {
List inventories = [ InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 ];
List items = [];
@@ -60,4 +77,35 @@ public static unsafe class InventoryState
return itemInfos;
}
+
+ public static List GetInventoryItems(string filterString, bool invert = false)
+ => GetInventoryItems().Where(item => item.IsRegexMatch(filterString) != invert).ToList();
+
+ private static uint GetItemUiCategoryKey(ItemInfo info)
+ => info.UiCategory.RowId;
+
+ private static CategoryInfo GetCategoryInfoForKey(uint key, ItemInfo? sample)
+ {
+ if (key == 0)
+ {
+ return new CategoryInfo
+ {
+ Name = "Misc",
+ Description = "Uncategorized items",
+ };
+ }
+
+ var uiCat = sample?.UiCategory.Value;
+ var name = uiCat?.Name.ToString();
+ if (string.IsNullOrWhiteSpace(name))
+ name = $"Category\\ {key}";
+
+ return new CategoryInfo
+ {
+ Name = name,
+ };
+ }
+
+
+
}
\ No newline at end of file
diff --git a/AetherBags/Inventory/ItemInfo.cs b/AetherBags/Inventory/ItemInfo.cs
index 41a7e1b..1ef6e50 100644
--- a/AetherBags/Inventory/ItemInfo.cs
+++ b/AetherBags/Inventory/ItemInfo.cs
@@ -3,6 +3,7 @@ using System.Numerics;
using System.Text.RegularExpressions;
using AetherBags.Extensions;
using FFXIVClientStructs.FFXIV.Client.Game;
+using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace AetherBags.Inventory;
@@ -25,7 +26,7 @@ public class ItemInfo : IEquatable {
public int Rarity => ItemData.Rarity;
- public int UiCategory => (int) ItemData.ItemUICategory.RowId;
+ public RowRef UiCategory => ItemData.ItemUICategory;
private string Description => ItemData.Description.ToString();
diff --git a/AetherBags/Nodes/HybridDirectionalStackNode.cs b/AetherBags/Nodes/HybridDirectionalStackNode.cs
new file mode 100644
index 0000000..f24ea58
--- /dev/null
+++ b/AetherBags/Nodes/HybridDirectionalStackNode.cs
@@ -0,0 +1,94 @@
+using System;
+using KamiToolKit;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes
+{
+ public class HybridDirectionalStackNode : LayoutListNode where T : NodeBase
+ {
+ public FlexGrowDirection GrowDirection
+ {
+ get;
+ set
+ {
+ field = value;
+ RecalculateLayout();
+ }
+ } = FlexGrowDirection.DownRight;
+
+ public bool Vertical
+ {
+ get;
+ set
+ {
+ field = value;
+ RecalculateLayout();
+ }
+ } = true;
+
+ public float Spacing
+ {
+ get;
+ set
+ {
+ field = value;
+ RecalculateLayout();
+ }
+ } = 1f;
+
+ public bool StretchCrossAxis
+ {
+ get;
+ set
+ {
+ field = value;
+ RecalculateLayout();
+ }
+ } = true;
+
+ protected override void InternalRecalculateLayout()
+ {
+ if (NodeList.Count == 0)
+ return;
+
+ bool alignRight = GrowDirection is FlexGrowDirection.DownLeft or FlexGrowDirection.UpLeft;
+ bool alignBottom = GrowDirection is FlexGrowDirection.UpRight or FlexGrowDirection.UpLeft;
+
+ float startX = alignRight ? Width : 0f;
+ float startY = alignBottom ? Height : 0f;
+
+ float cursor = 0f;
+
+ for (int i = 0; i < NodeList.Count; i++)
+ {
+ var node = NodeList[i];
+
+ if (StretchCrossAxis)
+ {
+ if (Vertical)
+ node.Width = Width;
+ else
+ node.Height = Height;
+ }
+
+ float x, y;
+ if (Vertical)
+ {
+ x = alignRight ? startX - node.Width : startX;
+ y = alignBottom ? startY - node.Height - cursor : startY + cursor;
+ cursor += node.Height + Spacing;
+ }
+ else
+ {
+ x = alignRight ? startX - node.Width - cursor : startX + cursor;
+ y = alignBottom ? startY - node.Height : startY;
+ cursor += node.Width + Spacing;
+ }
+
+ node.X = x;
+ node.Y = y;
+ AdjustNode(node);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/InventoryCategoryNode.cs b/AetherBags/Nodes/InventoryCategoryNode.cs
index b27c630..b682e80 100644
--- a/AetherBags/Nodes/InventoryCategoryNode.cs
+++ b/AetherBags/Nodes/InventoryCategoryNode.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Inventory;
+using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
@@ -14,6 +15,15 @@ public class InventoryCategoryNode : SimpleComponentNode
{
private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode _itemGridNode;
+
+ private const float ItemSize = 40;
+ private const float ItemHorizontalPadding = 6;
+ private const float ItemVerticalPadding = 6;
+ private const float HeaderHeight = 16;
+ private const float MinWidth = 40;
+
+ private float? _fixedWidth = null;
+
public InventoryCategoryNode()
{
_categoryNameTextNode = new TextNode
@@ -25,63 +35,101 @@ public class InventoryCategoryNode : SimpleComponentNode
_itemGridNode = new HybridDirectionalFlexNode
{
- Position = new Vector2(0, 16),
+ Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 100),
FillRowsFirst = true,
ItemsPerLine = 10,
- HorizontalPadding = 6,
- VerticalPadding = 6,
+ HorizontalPadding = ItemHorizontalPadding,
+ VerticalPadding = ItemVerticalPadding,
};
_itemGridNode.AttachNode(this);
}
- public required CategoryInfo Category
+ public required CategorizedInventory CategorizedInventory
{
get;
set
{
field = value;
- _categoryNameTextNode.String = value.Name;
- _categoryNameTextNode.TextColor = value.Color;
- _categoryNameTextNode.TooltipString = value.Description;
+ _categoryNameTextNode.String = value.Category.Name;
+ _categoryNameTextNode.TextColor = value.Category.Color;
+ _categoryNameTextNode.TooltipString = value.Category.Description;
+
+ UpdateItemGrid();
+ RecalculateSize();
}
}
- public required List Items
+ public void SetItemsPerLine(int itemsPerLine)
{
- get;
- set
- {
- field = value;
+ _itemGridNode.ItemsPerLine = itemsPerLine;
+ RecalculateSize();
+ }
- UpdateItemGrid();
+ public void SetFixedWidth(float width)
+ {
+ _fixedWidth = width;
+ RecalculateSize();
+ }
+
+ private void RecalculateSize()
+ {
+ int itemCount = CategorizedInventory.Items.Count;
+ if (itemCount == 0)
+ {
+ float width = _fixedWidth ?? MinWidth;
+ Size = new Vector2(width, HeaderHeight);
+ _categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = width };
+ return;
}
+
+ int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
+ int rows = (int)Math.Ceiling((float)itemCount / itemsPerLine);
+
+ float calculatedWidth;
+ if (_fixedWidth. HasValue)
+ {
+ calculatedWidth = _fixedWidth.Value;
+ }
+ else
+ {
+ int actualColumns = Math.Min(itemCount, itemsPerLine);
+ calculatedWidth = actualColumns * ItemSize + (actualColumns - 1) * ItemHorizontalPadding;
+ calculatedWidth = Math.Max(calculatedWidth, MinWidth);
+ }
+
+ float height = HeaderHeight + rows * ItemSize + (rows - 1) * ItemVerticalPadding;
+
+ Size = new Vector2(calculatedWidth, height);
+ _itemGridNode.Size = new Vector2(calculatedWidth, height - HeaderHeight);
+ _categoryNameTextNode.Size = _categoryNameTextNode.Size with { X = calculatedWidth };
}
private bool UpdateItemGrid()
{
- var listUpdated = _itemGridNode.SyncWithListData(Items, node => node.ItemInfo, data => CreateInventoryDragDropNode(data));
+ var listUpdated = _itemGridNode.SyncWithListData(CategorizedInventory.Items, node => node.ItemInfo, CreateInventoryDragDropNode);
return listUpdated;
}
- private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
+ private InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
+ InventoryItem item = data.Item;
InventoryDragDropNode node = new InventoryDragDropNode
{
Size = new Vector2(40),
IsVisible = true,
- IconId = data.IconId,
+ IconId = item.IconId,
AcceptedType = DragDropType.Nothing,
IsDraggable = false,
Payload = new DragDropPayload
{
Type = DragDropType.Item,
- Int1 = (int)data.Item.Container,
- Int2 = (int)data.Item.ItemId,
+ Int1 = (int)item.Container,
+ Int2 = (int)item.ItemId,
},
IsClickable = true,
- OnRollOver = node => node.ShowInventoryItemTooltip(data.Item.Container, data.Item.Slot),
+ OnRollOver = node => node.ShowInventoryItemTooltip(item.Container, item.Slot),
OnRollOut = node => node.HideTooltip(),
ItemInfo = data
};
diff --git a/AetherBags/Nodes/WrappingGridNode.cs b/AetherBags/Nodes/WrappingGridNode.cs
new file mode 100644
index 0000000..9b11941
--- /dev/null
+++ b/AetherBags/Nodes/WrappingGridNode.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using KamiToolKit;
+using KamiToolKit. Nodes;
+
+namespace AetherBags.Nodes
+{
+ public class WrappingGridNode : LayoutListNode where T : NodeBase
+ {
+ public float HorizontalSpacing { get; set; } = 10;
+ public float VerticalSpacing { get; set; } = 10;
+
+ private List> _rows = new();
+
+ protected override void InternalRecalculateLayout()
+ {
+ if (NodeList. Count == 0)
+ return;
+
+ _rows.Clear();
+
+ float availableWidth = Width;
+ float currentX = 0f;
+ float currentY = 0f;
+ float rowHeight = 0f;
+ List currentRow = new();
+
+ foreach (var node in NodeList)
+ {
+ float nodeWidth = node.Width;
+ float nodeHeight = node.Height;
+
+ if (currentX + nodeWidth > availableWidth && currentRow.Count > 0)
+ {
+ _rows.Add(currentRow);
+ currentRow = new();
+ currentY += rowHeight + VerticalSpacing;
+ currentX = 0f;
+ rowHeight = 0f;
+ }
+
+ node.X = currentX;
+ node. Y = currentY;
+ AdjustNode(node);
+
+ currentX += nodeWidth + HorizontalSpacing;
+ rowHeight = Math.Max(rowHeight, nodeHeight);
+ currentRow.Add(node);
+ }
+
+ if (currentRow.Count > 0)
+ {
+ _rows.Add(currentRow);
+ }
+ }
+
+ public float GetRequiredHeight()
+ {
+ if (NodeList.Count == 0)
+ return 0f;
+
+ float maxY = 0f;
+ foreach (var node in NodeList)
+ {
+ float nodeBottom = node.Y + node.Height;
+ maxY = Math. Max(maxY, nodeBottom);
+ }
+
+ return maxY;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs
index 9617931..5673c41 100644
--- a/AetherBags/Plugin.cs
+++ b/AetherBags/Plugin.cs
@@ -43,6 +43,7 @@ public class Plugin : IDalamudPlugin
if (Services.ClientState.IsLoggedIn) {
Services.Framework.RunOnFrameworkThread(OnLogin);
}
+ Services.AddonLifecycle.LogAddon("Inventory");
}
public void Dispose()