diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs
index 76cad89..f732071 100644
--- a/AetherBags/Addons/AddonInventoryWindow.cs
+++ b/AetherBags/Addons/AddonInventoryWindow.cs
@@ -4,6 +4,9 @@ using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Inventory;
using AetherBags.Nodes;
+using AetherBags.Nodes.Input;
+using AetherBags.Nodes.Inventory;
+using AetherBags.Nodes.Layout;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
diff --git a/AetherBags/Inventory/CurrencyInfo.cs b/AetherBags/Currency/CurrencyInfo.cs
similarity index 100%
rename from AetherBags/Inventory/CurrencyInfo.cs
rename to AetherBags/Currency/CurrencyInfo.cs
diff --git a/AetherBags/Currency/CurrencyState.cs b/AetherBags/Currency/CurrencyState.cs
new file mode 100644
index 0000000..c0a852a
--- /dev/null
+++ b/AetherBags/Currency/CurrencyState.cs
@@ -0,0 +1,149 @@
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Lumina.Excel.Sheets;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AetherBags.Currency;
+
+///
+/// Manages currency lookups, caching, and retrieval from the game.
+///
+public static unsafe class CurrencyState
+{
+ private const uint CurrencyIdLimitedTomestone = 0xFFFF_FFFE;
+ private const uint CurrencyIdNonLimitedTomestone = 0xFFFF_FFFD;
+
+ private static readonly Dictionary CurrencyItemByCurrencyIdCache = new(capacity: 32);
+ private static readonly Dictionary CurrencyStaticByItemIdCache = new(capacity: 64);
+
+ private static uint? _cachedLimitedTomestoneItemId;
+ private static uint? _cachedNonLimitedTomestoneItemId;
+
+ public static void InvalidateCaches()
+ {
+ CurrencyItemByCurrencyIdCache.Clear();
+ CurrencyStaticByItemIdCache.Clear();
+ _cachedLimitedTomestoneItemId = null;
+ _cachedNonLimitedTomestoneItemId = null;
+ }
+
+ public static IReadOnlyList GetCurrencyInfoList(uint[] currencyIds)
+ {
+ if (currencyIds.Length == 0)
+ return Array.Empty();
+
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ if (inventoryManager == null)
+ return Array.Empty();
+
+ List currencyInfoList = new List(currencyIds.Length);
+
+ for (int i = 0; i < currencyIds.Length; i++)
+ {
+ CurrencyItem currencyItem = ResolveCurrencyItemIdCached(currencyIds[i]);
+ if (currencyItem.ItemId == 0)
+ continue;
+
+ CurrencyStaticInfo staticInfo = GetCurrencyStaticInfoCached(currencyItem.ItemId);
+
+ uint amount = (uint)inventoryManager->GetInventoryItemCount(currencyItem.ItemId);
+
+ bool isCapped = false;
+ if (currencyItem.IsLimited)
+ {
+ int weeklyLimit = InventoryManager.GetLimitedTomestoneWeeklyLimit();
+ int weeklyAcquired = inventoryManager->GetWeeklyAcquiredTomestoneCount();
+ isCapped = weeklyAcquired >= weeklyLimit;
+ }
+
+ currencyInfoList.Add(new CurrencyInfo
+ {
+ Amount = amount,
+ MaxAmount = staticInfo.MaxAmount,
+ ItemId = staticInfo.ItemId,
+ IconId = staticInfo.IconId,
+ LimitReached = amount >= staticInfo.MaxAmount,
+ IsCapped = isCapped
+ });
+ }
+
+ return currencyInfoList;
+ }
+
+ private static uint? GetLimitedTomestoneItemIdCached()
+ {
+ if (_cachedLimitedTomestoneItemId.HasValue)
+ return _cachedLimitedTomestoneItemId.Value;
+
+ uint? itemId = Services.DataManager.GetExcelSheet()
+ .FirstOrDefault(t => t.Tomestones.RowId == 3)
+ .Item.RowId;
+
+ _cachedLimitedTomestoneItemId = itemId;
+ return itemId;
+ }
+
+ private static uint? GetNonLimitedTomestoneItemIdCached()
+ {
+ if (_cachedNonLimitedTomestoneItemId.HasValue)
+ return _cachedNonLimitedTomestoneItemId.Value;
+
+ uint? itemId = Services.DataManager.GetExcelSheet()
+ .FirstOrDefault(t => t.Tomestones.RowId == 2)
+ .Item.RowId;
+
+ _cachedNonLimitedTomestoneItemId = itemId;
+ return itemId;
+ }
+
+ private static CurrencyItem ResolveCurrencyItemIdCached(uint currencyId)
+ {
+ if (CurrencyItemByCurrencyIdCache.TryGetValue(currencyId, out var cached))
+ return cached;
+
+ uint itemId = currencyId;
+ bool isLimited = false;
+
+ if (currencyId == CurrencyIdLimitedTomestone)
+ {
+ itemId = GetLimitedTomestoneItemIdCached() ?? 0;
+ isLimited = true;
+ }
+ else if (currencyId == CurrencyIdNonLimitedTomestone)
+ {
+ itemId = GetNonLimitedTomestoneItemIdCached() ?? 0;
+ }
+
+ var resolved = new CurrencyItem(itemId, isLimited);
+ CurrencyItemByCurrencyIdCache[currencyId] = resolved;
+ return resolved;
+ }
+
+ private static CurrencyStaticInfo GetCurrencyStaticInfoCached(uint itemId)
+ {
+ if (CurrencyStaticByItemIdCache.TryGetValue(itemId, out CurrencyStaticInfo cached))
+ return cached;
+
+ var item = Services.DataManager.GetExcelSheet- ().GetRow(itemId);
+
+ var info = new CurrencyStaticInfo
+ {
+ ItemId = itemId,
+ IconId = item.Icon,
+ MaxAmount = item.StackSize,
+ };
+
+ CurrencyStaticByItemIdCache[itemId] = info;
+ return info;
+ }
+
+ private struct CurrencyStaticInfo
+ {
+ public uint ItemId;
+ public uint IconId;
+ public uint MaxAmount;
+ }
+
+ private record CurrencyItem(uint ItemId, bool IsLimited);
+}
\ No newline at end of file
diff --git a/AetherBags/Helpers/FileHelpers.cs b/AetherBags/Helpers/JsonFileHelper.cs
similarity index 98%
rename from AetherBags/Helpers/FileHelpers.cs
rename to AetherBags/Helpers/JsonFileHelper.cs
index bc27d01..7e9196f 100644
--- a/AetherBags/Helpers/FileHelpers.cs
+++ b/AetherBags/Helpers/JsonFileHelper.cs
@@ -5,7 +5,7 @@ using Dalamud.Utility;
namespace AetherBags.Helpers;
-public static class FileHelpers {
+public static class JsonFileHelper {
private static readonly JsonSerializerOptions SerializerOptions = new() {
WriteIndented = true,
IncludeFields = true,
diff --git a/AetherBags/Helpers/Util.cs b/AetherBags/Helpers/Util.cs
index 4d865fe..1e22183 100644
--- a/AetherBags/Helpers/Util.cs
+++ b/AetherBags/Helpers/Util.cs
@@ -80,14 +80,14 @@ public static class Util
public static void SaveConfig(SystemConfiguration config)
{
- FileInfo file = FileHelpers.GetFileInfo(SystemConfiguration.FileName);
- FileHelpers.SaveFile(config, file.FullName);
+ FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
+ JsonFileHelper.SaveFile(config, file.FullName);
}
private static SystemConfiguration LoadConfig()
{
- FileInfo file = FileHelpers.GetFileInfo(SystemConfiguration.FileName);
- return FileHelpers.LoadFile(file.FullName);
+ FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
+ return JsonFileHelper.LoadFile(file.FullName);
}
public static SystemConfiguration LoadConfigOrDefault()
diff --git a/AetherBags/Hooks/InventoryHook.cs b/AetherBags/Hooks/InventoryHook.cs
new file mode 100644
index 0000000..1ed98de
--- /dev/null
+++ b/AetherBags/Hooks/InventoryHook.cs
@@ -0,0 +1,60 @@
+using System;
+using Dalamud.Hooking;
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace AetherBags.Hooks;
+
+///
+/// Manages hooks related to inventory operations.
+///
+public sealed unsafe class InventoryHooks : IDisposable
+{
+ private delegate int MoveItemSlotDelegate(
+ InventoryManager* inventoryManager,
+ InventoryType srcContainer,
+ ushort srcSlot,
+ InventoryType dstContainer,
+ ushort dstSlot,
+ bool unk);
+
+ private readonly Hook? _moveItemSlotHook;
+
+ public InventoryHooks()
+ {
+ try
+ {
+ _moveItemSlotHook = Services.GameInteropProvider.HookFromSignature(
+ "E8 ?? ?? ?? ?? 48 8B 03 66 FF C5",
+ MoveItemSlotDetour);
+ _moveItemSlotHook.Enable();
+
+ Services.Logger.Debug("MoveItemSlot hooked successfully.");
+ }
+ catch (Exception e)
+ {
+ Services.Logger.Error(e, "Failed to hook MoveItemSlot");
+ }
+ }
+
+ private int MoveItemSlotDetour(
+ InventoryManager* manager,
+ InventoryType srcType,
+ ushort srcSlot,
+ InventoryType dstType,
+ ushort dstSlot,
+ bool unk)
+ {
+ InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
+ InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
+
+ Services.Logger.Info(
+ $"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
+
+ return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
+ }
+
+ public void Dispose()
+ {
+ _moveItemSlotHook?.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/CategoryBucketManager.cs b/AetherBags/Inventory/CategoryBucketManager.cs
new file mode 100644
index 0000000..f87ead0
--- /dev/null
+++ b/AetherBags/Inventory/CategoryBucketManager.cs
@@ -0,0 +1,297 @@
+using AetherBags.Configuration;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace AetherBags.Inventory;
+
+public static class CategoryBucketManager
+{
+ private const uint UserCategoryKeyFlag = 0x8000_0000;
+
+ private static readonly Dictionary CategoryInfoCache = new(capacity: 256);
+
+ public static uint MakeUserCategoryKey(int order)
+ => UserCategoryKeyFlag | (uint)(order & 0x7FFF_FFFF);
+
+ public static bool IsUserCategoryKey(uint key)
+ => (key & UserCategoryKeyFlag) != 0;
+
+ ///
+ /// Resets all buckets for a new refresh cycle.
+ ///
+ public static void ResetBuckets(Dictionary bucketsByKey)
+ {
+ foreach (var kvp in bucketsByKey)
+ {
+ CategoryBucket bucket = kvp.Value;
+ bucket.Used = false;
+ bucket.Items.Clear();
+ bucket.FilteredItems.Clear();
+ }
+ }
+
+ public static void BucketByUserCategories(
+ Dictionary itemInfoByKey,
+ List userCategories,
+ Dictionary bucketsByKey,
+ HashSet claimedKeys,
+ List sortedScratch)
+ {
+ sortedScratch.Clear();
+ sortedScratch.AddRange(userCategories);
+ sortedScratch.Sort((left, right) =>
+ {
+ int priority = left.Priority.CompareTo(right.Priority);
+ if (priority != 0) return priority;
+
+ int order = left.Order.CompareTo(right.Order);
+ if (order != 0) return order;
+
+ return string.Compare(left.Id, right.Id, StringComparison.OrdinalIgnoreCase);
+ });
+
+ for (int i = 0; i < sortedScratch.Count; i++)
+ {
+ UserCategoryDefinition category = sortedScratch[i];
+ uint bucketKey = MakeUserCategoryKey(category.Order);
+
+ if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
+ {
+ bucket = new CategoryBucket
+ {
+ Key = bucketKey,
+ Category = new CategoryInfo
+ {
+ Name = category.Name,
+ Description = category.Description,
+ Color = category.Color,
+ },
+ Items = new List(capacity: 16),
+ FilteredItems = new List(capacity: 16),
+ Used = true,
+ };
+ bucketsByKey.Add(bucketKey, bucket);
+ }
+ else
+ {
+ bucket.Used = true;
+ bucket.Category.Name = category.Name;
+ bucket.Category.Description = category.Description;
+ bucket.Category.Color = category.Color;
+ }
+
+ foreach (var itemKvp in itemInfoByKey)
+ {
+ ulong itemKey = itemKvp.Key;
+ ItemInfo item = itemKvp.Value;
+
+ if (claimedKeys.Contains(itemKey))
+ continue;
+
+ if (UserCategoryMatcher.Matches(item, category))
+ {
+ bucket.Items.Add(item);
+ claimedKeys.Add(itemKey);
+ }
+ }
+
+ if (bucket.Items.Count == 0)
+ bucket.Used = false;
+ }
+ }
+
+ public static void BucketByGameCategories(
+ Dictionary itemInfoByKey,
+ Dictionary bucketsByKey,
+ HashSet claimedKeys,
+ bool userCategoriesEnabled)
+ {
+ foreach (var itemKvp in itemInfoByKey)
+ {
+ ulong itemKey = itemKvp.Key;
+ ItemInfo info = itemKvp.Value;
+
+ if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
+ continue;
+
+ uint categoryKey = info.UiCategory.RowId;
+
+ if (!bucketsByKey.TryGetValue(categoryKey, out CategoryBucket? bucket))
+ {
+ bucket = new CategoryBucket
+ {
+ Key = categoryKey,
+ Category = GetCategoryInfoCached(categoryKey, info),
+ Items = new List(capacity: 16),
+ FilteredItems = new List(capacity: 16),
+ Used = true,
+ };
+ bucketsByKey.Add(categoryKey, bucket);
+ }
+ else
+ {
+ bucket.Used = true;
+ }
+
+ bucket.Items.Add(info);
+ }
+ }
+
+ public static void BucketUnclaimedToMisc(
+ Dictionary itemInfoByKey,
+ Dictionary bucketsByKey,
+ HashSet claimedKeys,
+ bool userCategoriesEnabled)
+ {
+ if (!bucketsByKey.TryGetValue(0u, out CategoryBucket? miscBucket))
+ {
+ CategoryInfo miscInfo;
+ if (itemInfoByKey.Count > 0)
+ {
+ var sample = itemInfoByKey.Values.First();
+ miscInfo = GetCategoryInfoCached(0u, sample);
+ }
+ else
+ {
+ miscInfo = new CategoryInfo { Name = "Misc", Description = "Uncategorized items" };
+ }
+
+ miscBucket = new CategoryBucket
+ {
+ Key = 0u,
+ Category = miscInfo,
+ Items = new List(capacity: 16),
+ FilteredItems = new List(capacity: 16),
+ Used = true,
+ };
+ bucketsByKey.Add(0u, miscBucket);
+ }
+ else
+ {
+ miscBucket.Used = true;
+ }
+
+ foreach (var itemKvp in itemInfoByKey)
+ {
+ ulong itemKey = itemKvp.Key;
+ ItemInfo info = itemKvp.Value;
+
+ if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
+ continue;
+
+ miscBucket.Items.Add(info);
+ }
+
+ if (miscBucket.Items.Count == 0)
+ miscBucket.Used = false;
+ }
+
+ public static void SortBucketsAndBuildKeyList(
+ Dictionary bucketsByKey,
+ List sortedCategoryKeys)
+ {
+ sortedCategoryKeys.Clear();
+
+ foreach (var kvp in bucketsByKey)
+ {
+ CategoryBucket bucket = kvp.Value;
+ if (!bucket.Used)
+ continue;
+
+ bucket.Items.Sort(ItemCountDescComparer.Instance);
+ sortedCategoryKeys.Add(bucket.Key);
+ }
+
+ sortedCategoryKeys.Sort((left, right) =>
+ {
+ bool leftCategory = IsUserCategoryKey(left);
+ bool rightCategory = IsUserCategoryKey(right);
+ if (leftCategory != rightCategory) return leftCategory ? -1 : 1;
+ return left.CompareTo(right);
+ });
+ }
+
+ public static void BuildCategorizedList(
+ Dictionary bucketsByKey,
+ List sortedCategoryKeys,
+ List allCategories)
+ {
+ allCategories.Clear();
+ allCategories.Capacity = Math.Max(allCategories.Capacity, sortedCategoryKeys.Count);
+
+ for (int i = 0; i < sortedCategoryKeys.Count; i++)
+ {
+ uint key = sortedCategoryKeys[i];
+ CategoryBucket bucket = bucketsByKey[key];
+ allCategories.Add(new CategorizedInventory(bucket.Key, bucket.Category, bucket.Items));
+ }
+
+ int displayed = 0;
+ for (int i = 0; i < allCategories.Count; i++)
+ displayed += allCategories[i].Items.Count;
+
+ Services.Logger.DebugOnly($"AllCategories={allCategories.Count} DisplayedItemsTotal={displayed}");
+ }
+
+ private static CategoryInfo GetCategoryInfoCached(uint key, ItemInfo sample)
+ {
+ if (CategoryInfoCache.TryGetValue(key, out var cached))
+ return cached;
+
+ CategoryInfo info = GetCategoryInfoSlow(key, sample);
+ CategoryInfoCache[key] = info;
+ return info;
+ }
+
+ private static CategoryInfo GetCategoryInfoSlow(uint key, ItemInfo sample)
+ {
+ if (key == 0)
+ {
+ return new CategoryInfo
+ {
+ Name = "Misc",
+ Description = "Uncategorized items",
+ };
+ }
+
+ var uiCat = sample.UiCategory.Value;
+ string name = uiCat.Name.ToString();
+
+ if (string.IsNullOrWhiteSpace(name))
+ name = $"Category {key}";
+
+ return new CategoryInfo
+ {
+ Name = name,
+ };
+ }
+}
+
+public sealed class CategoryBucket
+{
+ public uint Key;
+ public CategoryInfo Category = null!;
+ public List Items = null!;
+ public List FilteredItems = null!;
+ public bool Used;
+}
+
+public sealed class ItemCountDescComparer : IComparer
+{
+ public static readonly ItemCountDescComparer Instance = new();
+
+ public int Compare(ItemInfo? left, ItemInfo? right)
+ {
+ if (ReferenceEquals(left, right)) return 0;
+ if (left is null) return 1;
+ if (right is null) return -1;
+
+ int leftCount = left.ItemCount;
+ int rightCount = right.ItemCount;
+
+ if (leftCount > rightCount) return -1;
+ if (leftCount < rightCount) return 1;
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryFilter.cs b/AetherBags/Inventory/InventoryFilter.cs
new file mode 100644
index 0000000..96b2545
--- /dev/null
+++ b/AetherBags/Inventory/InventoryFilter.cs
@@ -0,0 +1,66 @@
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace AetherBags.Inventory;
+
+public static class InventoryFilter
+{
+ public static IReadOnlyList FilterCategories(
+ IReadOnlyList allCategories,
+ Dictionary bucketsByKey,
+ List filteredCategories,
+ string filterString,
+ bool invert = false)
+ {
+ if (string.IsNullOrEmpty(filterString))
+ return allCategories;
+
+ Regex? re = null;
+ bool regexValid = true;
+
+ try
+ {
+ re = new Regex(filterString, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ }
+ catch
+ {
+ regexValid = false;
+ }
+
+ filteredCategories.Clear();
+
+ for (int i = 0; i < allCategories.Count; i++)
+ {
+ CategorizedInventory cat = allCategories[i];
+ CategoryBucket bucket = bucketsByKey[cat.Key];
+
+ var filtered = bucket.FilteredItems;
+ filtered.Clear();
+
+ var src = bucket.Items;
+ for (int j = 0; j < src.Count; j++)
+ {
+ ItemInfo info = src[j];
+
+ bool isMatch;
+ if (regexValid)
+ {
+ isMatch = info.IsRegexMatch(re!);
+ }
+ else
+ {
+ isMatch = info.Name.Contains(filterString, StringComparison.OrdinalIgnoreCase) || info.DescriptionContains(filterString);
+ }
+
+ if (isMatch != invert)
+ filtered.Add(info);
+ }
+
+ if (filtered.Count != 0)
+ filteredCategories.Add(new CategorizedInventory(bucket.Key, bucket.Category, filtered));
+ }
+
+ return filteredCategories;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryScanner.cs b/AetherBags/Inventory/InventoryScanner.cs
new file mode 100644
index 0000000..0bb921b
--- /dev/null
+++ b/AetherBags/Inventory/InventoryScanner.cs
@@ -0,0 +1,179 @@
+using AetherBags.Configuration;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using System.Collections.Generic;
+
+namespace AetherBags.Inventory;
+
+public static unsafe class InventoryScanner
+{
+ private static readonly InventoryType[] BagInventories =
+ [
+ InventoryType.Inventory1,
+ InventoryType.Inventory2,
+ InventoryType.Inventory3,
+ InventoryType.Inventory4,
+ ];
+
+ public static readonly InventoryType[] StandardInventories =
+ [
+ InventoryType.Inventory1,
+ InventoryType.Inventory2,
+ InventoryType.Inventory3,
+ InventoryType.Inventory4,
+ InventoryType.EquippedItems,
+ InventoryType.ArmoryMainHand,
+ InventoryType.ArmoryHead,
+ InventoryType.ArmoryBody,
+ InventoryType.ArmoryHands,
+ InventoryType.ArmoryWaist,
+ InventoryType.ArmoryLegs,
+ InventoryType.ArmoryFeets,
+ InventoryType.ArmoryOffHand,
+ InventoryType.ArmoryEar,
+ InventoryType.ArmoryNeck,
+ InventoryType.ArmoryWrist,
+ InventoryType.ArmoryRings,
+ InventoryType.Currency,
+ InventoryType.Crystals,
+ InventoryType.ArmorySoulCrystal,
+ ];
+
+ private const ulong AggregatedKeyTag = 1UL << 63;
+
+ public static ulong MakeAggregatedItemKey(uint itemId, bool isHighQuality)
+ => AggregatedKeyTag | ((ulong)itemId << 1) | (isHighQuality ? 1UL : 0UL);
+
+ public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
+ => ((ulong)(uint)container << 32) | (uint)slot;
+
+ public static void ScanBags(
+ InventoryManager* inventoryManager,
+ InventoryStackMode stackMode,
+ Dictionary aggByKey)
+ {
+ aggByKey.Clear();
+
+ int scannedSlots = 0;
+ int nonEmptySlots = 0;
+ int collisions = 0;
+
+ for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
+ {
+ var inventoryType = BagInventories[inventoryIndex];
+ var container = inventoryManager->GetInventoryContainer(inventoryType);
+ if (container == null)
+ {
+ Services.Logger.DebugOnly($"Container null: {inventoryType}");
+ continue;
+ }
+
+ int size = container->Size;
+ Services.Logger.DebugOnly($"Scanning {inventoryType} Size={size}");
+
+ for (int slot = 0; slot < size; slot++)
+ {
+ scannedSlots++;
+
+ ref var item = ref container->Items[slot];
+ uint id = item.ItemId;
+ if (id == 0)
+ continue;
+
+ nonEmptySlots++;
+
+ int quantity = item.Quantity;
+ bool isHq = (item.Flags & InventoryItem.ItemFlags.HighQuality) != 0;
+
+ ulong key = stackMode == InventoryStackMode.AggregateByItemId
+ ? MakeAggregatedItemKey(id, isHq)
+ : MakeNaturalSlotKey(inventoryType, slot);
+
+ Services.Logger.DebugOnly($"Slot {inventoryType}[{slot}] ItemId={id} Qty={quantity} Key=0x{key: X16}");
+
+ if (aggByKey.TryGetValue(key, out AggregatedItem agg))
+ {
+ if (stackMode == InventoryStackMode.NaturalStacks)
+ {
+ collisions++;
+ Services.Logger.DebugOnly($"COLLISION Key=0x{key:X16}: existing ItemId={agg.First.ItemId} new ItemId={id}");
+ }
+
+ agg.Total += quantity;
+ aggByKey[key] = agg;
+ }
+ else
+ {
+ aggByKey.Add(key, new AggregatedItem { First = item, Total = quantity });
+ }
+ }
+ }
+
+ Services.Logger.DebugOnly($"ScannedSlots={scannedSlots} NonEmptySlots={nonEmptySlots} AggByKey.Count={aggByKey.Count} Collisions={collisions}");
+ }
+
+ public static void BuildItemInfos(
+ Dictionary aggByKey,
+ Dictionary itemInfoByKey)
+ {
+ foreach (var kvp in aggByKey)
+ {
+ ulong key = kvp.Key;
+ AggregatedItem agg = kvp.Value;
+
+ if (!itemInfoByKey.TryGetValue(key, out ItemInfo? info))
+ {
+ info = new ItemInfo
+ {
+ Key = key,
+ Item = agg.First,
+ ItemCount = agg.Total,
+ };
+ itemInfoByKey.Add(key, info);
+ }
+ else
+ {
+ info.Item = agg.First;
+ info.ItemCount = agg.Total;
+ }
+ }
+
+ Services.Logger.DebugOnly($"ItemInfoByKey.Count={itemInfoByKey.Count}");
+ }
+
+ public static void PruneStaleItemInfos(
+ Dictionary aggByKey,
+ Dictionary itemInfoByKey,
+ List removeKeysScratch)
+ {
+ if (itemInfoByKey.Count == aggByKey.Count)
+ return;
+
+ removeKeysScratch.Clear();
+
+ foreach (var kvp in itemInfoByKey)
+ {
+ ulong key = kvp.Key;
+ if (!aggByKey.ContainsKey(key))
+ removeKeysScratch.Add(key);
+ }
+
+ for (int i = 0; i < removeKeysScratch.Count; i++)
+ itemInfoByKey.Remove(removeKeysScratch[i]);
+ }
+
+ public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
+ => InventoryManager.Instance()->GetInventoryContainer(inventoryType);
+
+ public static string GetEmptyItemSlotsString()
+ {
+ uint empty = InventoryManager.Instance()->GetEmptySlotsInBag();
+ uint used = 140 - empty;
+ return $"{used}/140";
+ }
+}
+
+public struct AggregatedItem
+{
+ public InventoryItem First;
+ public int Total;
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/InventoryState.cs
index 5de62e4..b5599fa 100644
--- a/AetherBags/Inventory/InventoryState.cs
+++ b/AetherBags/Inventory/InventoryState.cs
@@ -1,82 +1,30 @@
using AetherBags.Configuration;
-using AetherBags.Currency;
using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
-using Lumina.Excel.Sheets;
-using System;
using System.Collections.Generic;
using System.Linq;
-using System.Text.RegularExpressions;
+using AetherBags.Currency;
+using CurrencyManager = FFXIVClientStructs.FFXIV.Client.Game.CurrencyManager;
namespace AetherBags.Inventory;
public static unsafe class InventoryState
{
- public static readonly InventoryType[] StandardInventories =
- [
- InventoryType.Inventory1,
- InventoryType.Inventory2,
- InventoryType.Inventory3,
- InventoryType.Inventory4,
- InventoryType.EquippedItems,
- InventoryType.ArmoryMainHand,
- InventoryType.ArmoryHead,
- InventoryType.ArmoryBody,
- InventoryType.ArmoryHands,
- InventoryType.ArmoryWaist,
- InventoryType.ArmoryLegs,
- InventoryType.ArmoryFeets,
- InventoryType.ArmoryOffHand,
- InventoryType.ArmoryEar,
- InventoryType.ArmoryNeck,
- InventoryType.ArmoryWrist,
- InventoryType.ArmoryRings,
- InventoryType.Currency,
- InventoryType.Crystals,
- InventoryType.ArmorySoulCrystal,
- ];
-
- private static readonly InventoryType[] BagInventories =
- [
- InventoryType.Inventory1,
- InventoryType.Inventory2,
- InventoryType.Inventory3,
- InventoryType.Inventory4,
- ];
-
- private static readonly Dictionary CategoryInfoCache = new(capacity: 256);
+ public static IReadOnlyList StandardInventories => InventoryScanner.StandardInventories;
private static readonly Dictionary AggByKey = new(capacity: 512);
private static readonly Dictionary ItemInfoByKey = new(capacity: 512);
-
private static readonly Dictionary BucketsByKey = new(capacity: 256);
private static readonly List SortedCategoryKeys = new(capacity: 256);
-
private static readonly List AllCategories = new(capacity: 256);
private static readonly List FilteredCategories = new(capacity: 256);
-
private static readonly List UserCategoriesSortedScratch = new(capacity: 64);
- private static readonly List RemoveKeysScratch = new(capacity: 256);
-
- private const uint UserCategoryKeyFlag = 0x8000_0000;
-
- private const ulong AggregatedKeyTag = 1UL << 63;
-
- private static uint MakeUserCategoryKey(int order)
- => UserCategoryKeyFlag | (uint)(order & 0x7FFF_FFFF);
-
- private static bool IsUserCategoryKey(uint key)
- => (key & UserCategoryKeyFlag) != 0;
-
- private static ulong MakeAggregatedItemKey(uint itemId, bool isHighQuality)
- => AggregatedKeyTag | ((ulong)itemId << 1) | (isHighQuality ? 1UL : 0UL);
+ private static readonly List RemoveKeysScratch = new(capacity: 256);
+ private static readonly HashSet ClaimedKeys = new(capacity: 512);
public static bool Contains(this IReadOnlyCollection inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type);
- private static ulong MakeNaturalSlotKey(InventoryType container, int slot)
- => ((ulong)(uint)container << 32) | (uint)slot;
-
public static void RefreshFromGame()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
@@ -87,497 +35,77 @@ public static unsafe class InventoryState
}
var config = System.Config;
-
InventoryStackMode stackMode = config.General.StackMode;
-
bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled;
bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled;
List userCategories = config.Categories.UserCategories;
+ Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}");
+
AggByKey.Clear();
ItemInfoByKey.Clear();
-
- BucketsByKey.Clear();
SortedCategoryKeys.Clear();
AllCategories.Clear();
FilteredCategories.Clear();
+ ClaimedKeys.Clear();
- Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}");
-
- int scannedSlots = 0;
- int nonEmptySlots = 0;
- int collisions = 0;
-
- for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
- {
- var inventoryType = BagInventories[inventoryIndex];
- var container = inventoryManager->GetInventoryContainer(inventoryType);
- if (container == null)
- {
- Services.Logger.DebugOnly($"Container null: {inventoryType}");
- continue;
- }
-
- int size = container->Size;
- Services.Logger.DebugOnly($"Scanning {inventoryType} Size={size}");
-
- for (int slot = 0; slot < size; slot++)
- {
- scannedSlots++;
-
- ref var item = ref container->Items[slot];
- uint id = item.ItemId;
- if (id == 0)
- continue;
-
- nonEmptySlots++;
-
- int quantity = item.Quantity;
- bool isHq = (item.Flags & InventoryItem.ItemFlags.HighQuality) != 0;
-
- ulong key = stackMode == InventoryStackMode.AggregateByItemId
- ? MakeAggregatedItemKey(id, isHq)
- : MakeNaturalSlotKey(inventoryType, slot);
-
- Services.Logger.DebugOnly($"Slot {inventoryType}[{slot}] ItemId={id} Qty={quantity} Key=0x{key:X16}");
-
- if (AggByKey.TryGetValue(key, out AggregatedItem agg))
- {
- if (stackMode == InventoryStackMode.NaturalStacks)
- {
- collisions++;
- Services.Logger.DebugOnly($"COLLISION Key=0x{key:X16}: existing ItemId={agg.First.ItemId} new ItemId={id}");
- }
-
- agg.Total += quantity;
- AggByKey[key] = agg;
- }
- else
- {
- AggByKey.Add(key, new AggregatedItem { First = item, Total = quantity });
- }
- }
- }
-
- Services.Logger.DebugOnly($"ScannedSlots={scannedSlots} NonEmptySlots={nonEmptySlots} AggByKey.Count={AggByKey.Count} Collisions={collisions}");
-
- foreach (var kvp in BucketsByKey)
- {
- CategoryBucket bucket = kvp.Value;
- bucket.Used = false;
- bucket.Items.Clear();
- bucket.FilteredItems.Clear();
- }
-
- foreach (var kvp in AggByKey)
- {
- ulong key = kvp.Key;
- AggregatedItem agg = kvp.Value;
-
- if (!ItemInfoByKey.TryGetValue(key, out ItemInfo? info))
- {
- info = new ItemInfo
- {
- Key = key,
- Item = agg.First,
- ItemCount = agg.Total,
- };
- ItemInfoByKey.Add(key, info);
- }
- else
- {
- info.Item = agg.First;
- info.ItemCount = agg.Total;
- }
- }
-
- Services.Logger.DebugOnly($"ItemInfoByKey.Count={ItemInfoByKey.Count}");
-
- // Bucket by user category
- HashSet claimedKeys = new HashSet(capacity: ItemInfoByKey.Count);
+ InventoryScanner.ScanBags(inventoryManager, stackMode, AggByKey);
+ CategoryBucketManager.ResetBuckets(BucketsByKey);
+ InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey);
if (userCategoriesEnabled && userCategories.Count > 0)
{
- UserCategoriesSortedScratch.Clear();
- UserCategoriesSortedScratch.AddRange(userCategories);
- UserCategoriesSortedScratch.Sort((a, b) =>
- {
- int p = a.Priority.CompareTo(b.Priority);
- if (p != 0) return p;
-
- int o = a.Order.CompareTo(b.Order);
- if (o != 0) return o;
-
- return string.Compare(a.Id, b.Id, StringComparison.OrdinalIgnoreCase);
- });
-
- for (int c = 0; c < UserCategoriesSortedScratch.Count; c++)
- {
- UserCategoryDefinition category = UserCategoriesSortedScratch[c];
- uint bucketKey = MakeUserCategoryKey(category.Order);
-
- if (!BucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
- {
- bucket = new CategoryBucket
- {
- Key = bucketKey,
- Category = new CategoryInfo
- {
- Name = category.Name,
- Description = category.Description,
- Color = category.Color,
- },
- Items = new List(capacity: 16),
- FilteredItems = new List(capacity: 16),
- Used = true,
- };
- BucketsByKey.Add(bucketKey, bucket);
- }
- else
- {
- bucket.Used = true;
- bucket.Category.Name = category.Name;
- bucket.Category.Description = category.Description;
- bucket.Category.Color = category.Color;
- }
-
- foreach (var itemKvp in ItemInfoByKey)
- {
- ulong itemKey = itemKvp.Key;
- ItemInfo item = itemKvp.Value;
-
- if (claimedKeys.Contains(itemKey))
- continue;
-
- if (UserCategoryMatcher.Matches(item, category))
- {
- bucket.Items.Add(item);
- claimedKeys.Add(itemKey);
- }
- }
-
- if (bucket.Items.Count == 0)
- bucket.Used = false;
- }
+ CategoryBucketManager.BucketByUserCategories(
+ ItemInfoByKey,
+ userCategories,
+ BucketsByKey,
+ ClaimedKeys,
+ UserCategoriesSortedScratch);
}
- // Game category bucket
if (gameCategoriesEnabled)
{
- foreach (var itemKvp in ItemInfoByKey)
- {
- ulong itemKey = itemKvp.Key;
- ItemInfo info = itemKvp.Value;
-
- if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
- continue;
-
- uint categoryKey = info.UiCategory.RowId;
-
- if (!BucketsByKey.TryGetValue(categoryKey, out CategoryBucket? bucket))
- {
- bucket = new CategoryBucket
- {
- Key = categoryKey,
- Category = GetCategoryInfoForKeyCached(categoryKey, info),
- Items = new List(capacity: 16),
- FilteredItems = new List(capacity: 16),
- Used = true,
- };
- BucketsByKey.Add(categoryKey, bucket);
- }
- else
- {
- bucket.Used = true;
- }
-
- bucket.Items.Add(info);
- }
+ CategoryBucketManager.BucketByGameCategories(
+ ItemInfoByKey,
+ BucketsByKey,
+ ClaimedKeys,
+ userCategoriesEnabled);
+ }
+ else
+ {
+ CategoryBucketManager.BucketUnclaimedToMisc(
+ ItemInfoByKey,
+ BucketsByKey,
+ ClaimedKeys,
+ userCategoriesEnabled);
}
- // Unclaimed items
- if (!gameCategoriesEnabled)
- {
- if (!BucketsByKey.TryGetValue(0u, out CategoryBucket? miscBucket))
- {
- CategoryInfo miscInfo;
- if (ItemInfoByKey.Count > 0)
- {
- var sample = ItemInfoByKey.Values.First();
- miscInfo = GetCategoryInfoForKeyCached(0u, sample);
- }
- else
- {
- miscInfo = new CategoryInfo { Name = "Misc", Description = "Uncategorized items" };
- }
-
- miscBucket = new CategoryBucket
- {
- Key = 0u,
- Category = miscInfo,
- Items = new List(capacity: 16),
- FilteredItems = new List(capacity: 16),
- Used = true,
- };
- BucketsByKey.Add(0u, miscBucket);
- }
- else
- {
- miscBucket.Used = true;
- }
-
- foreach (var itemKvp in ItemInfoByKey)
- {
- ulong itemKey = itemKvp.Key;
- ItemInfo info = itemKvp.Value;
-
- if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
- continue;
-
- miscBucket.Items.Add(info);
- }
-
- if (miscBucket.Items.Count == 0)
- miscBucket.Used = false;
- }
-
- if (ItemInfoByKey.Count != AggByKey.Count)
- {
- RemoveKeysScratch.Clear();
-
- foreach (var kvp in ItemInfoByKey)
- {
- ulong key = kvp.Key;
- if (!AggByKey.ContainsKey(key))
- RemoveKeysScratch.Add(key);
- }
-
- for (int i = 0; i < RemoveKeysScratch.Count; i++)
- ItemInfoByKey.Remove(RemoveKeysScratch[i]);
- }
-
- SortedCategoryKeys.Clear();
-
- foreach (var kvp in BucketsByKey)
- {
- CategoryBucket bucket = kvp.Value;
- if (!bucket.Used)
- continue;
-
- bucket.Items.Sort(ItemCountDescComparer.Instance);
- SortedCategoryKeys.Add(bucket.Key);
- }
-
- SortedCategoryKeys.Sort((a, b) =>
- {
- bool au = IsUserCategoryKey(a);
- bool bu = IsUserCategoryKey(b);
- if (au != bu) return au ? -1 : 1;
- return a.CompareTo(b);
- });
-
- AllCategories.Clear();
- AllCategories.Capacity = Math.Max(AllCategories.Capacity, SortedCategoryKeys.Count);
-
- for (int i = 0; i < SortedCategoryKeys.Count; i++)
- {
- uint key = SortedCategoryKeys[i];
- CategoryBucket bucket = BucketsByKey[key];
- AllCategories.Add(new CategorizedInventory(bucket.Key, bucket.Category, bucket.Items));
- }
- int displayed = 0;
- for (int i = 0; i < AllCategories.Count; i++)
- displayed += AllCategories[i].Items.Count;
-
- Services.Logger.DebugOnly($"AllCategories={AllCategories.Count} DisplayedItemsTotal={displayed}");
+ InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch);
+ CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys);
+ CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories);
}
public static IReadOnlyList GetInventoryItemCategories(string filterString = "", bool invert = false)
{
- if (string.IsNullOrEmpty(filterString))
- return AllCategories;
-
- Regex? re = null;
- bool regexValid = true;
-
- try
- {
- re = new Regex(filterString, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
- }
- catch
- {
- regexValid = false;
- }
-
- FilteredCategories.Clear();
-
- for (int i = 0; i < AllCategories.Count; i++)
- {
- CategorizedInventory cat = AllCategories[i];
- CategoryBucket bucket = BucketsByKey[cat.Key];
-
- var filtered = bucket.FilteredItems;
- filtered.Clear();
-
- var src = bucket.Items;
- for (int j = 0; j < src.Count; j++)
- {
- ItemInfo info = src[j];
-
- bool isMatch;
- if (regexValid)
- {
- isMatch = info.IsRegexMatch(re!);
- }
- else
- {
- isMatch = info.Name.Contains(filterString, StringComparison.OrdinalIgnoreCase)
- || info.DescriptionContains(filterString);
- }
-
- if (isMatch != invert)
- filtered.Add(info);
- }
-
- if (filtered.Count != 0)
- FilteredCategories.Add(new CategorizedInventory(bucket.Key, bucket.Category, filtered));
- }
-
- return FilteredCategories;
+ return InventoryFilter.FilterCategories(
+ AllCategories,
+ BucketsByKey,
+ FilteredCategories,
+ filterString,
+ invert);
}
public static string GetEmptyItemSlotsString()
- {
- uint empty = InventoryManager.Instance()->GetEmptySlotsInBag();
- uint used = 140 - empty;
- return $"{used}/140";
- }
-
- private const uint CurrencyIdLimitedTomestone = 0xFFFF_FFFE;
- private const uint CurrencyIdNonLimitedTomestone = 0xFFFF_FFFD;
-
- private static readonly Dictionary CurrencyItemByCurrencyIdCache = new(capacity: 32);
-
- private static readonly Dictionary CurrencyStaticByItemIdCache = new(capacity: 64);
-
- private static uint? CachedLimitedTomestoneItemId;
- private static uint? CachedNonLimitedTomestoneItemId;
-
- public static void InvalidateCurrencyCaches()
- {
- CurrencyItemByCurrencyIdCache.Clear();
- CurrencyStaticByItemIdCache.Clear();
- CachedLimitedTomestoneItemId = null;
- CachedNonLimitedTomestoneItemId = null;
- }
-
- private static uint? GetLimitedTomestoneItemIdCached()
- {
- if (CachedLimitedTomestoneItemId.HasValue)
- return CachedLimitedTomestoneItemId.Value;
-
- uint? itemId = Services.DataManager.GetExcelSheet()
- .FirstOrDefault(t => t.Tomestones.RowId == 3)
- .Item.RowId;
-
- CachedLimitedTomestoneItemId = itemId;
- return itemId;
- }
-
- private static uint? GetNonLimitedTomestoneItemIdCached()
- {
- if (CachedNonLimitedTomestoneItemId.HasValue)
- return CachedNonLimitedTomestoneItemId.Value;
-
- uint? itemId = Services.DataManager.GetExcelSheet()
- .FirstOrDefault(t => t.Tomestones.RowId == 2)
- .Item.RowId;
-
- CachedNonLimitedTomestoneItemId = itemId;
- return itemId;
- }
-
- private static CurrencyItem ResolveCurrencyItemIdCached(uint currencyId)
- {
- if (CurrencyItemByCurrencyIdCache.TryGetValue(currencyId, out CurrencyItem cached))
- return cached;
-
- uint itemId = currencyId;
- bool isLimited = false;
-
- if (currencyId == CurrencyIdLimitedTomestone)
- {
- itemId = GetLimitedTomestoneItemIdCached() ?? 0;
- isLimited = true;
- }
- else if (currencyId == CurrencyIdNonLimitedTomestone)
- {
- itemId = GetNonLimitedTomestoneItemIdCached() ?? 0;
- }
-
- var resolved = new CurrencyItem(itemId, isLimited);
- CurrencyItemByCurrencyIdCache[currencyId] = resolved;
- return resolved;
- }
-
- private static CurrencyStaticInfo GetCurrencyStaticInfoCached(uint itemId)
- {
- if (CurrencyStaticByItemIdCache.TryGetValue(itemId, out CurrencyStaticInfo cached))
- return cached;
-
- var item = Services.DataManager.GetExcelSheet
- ().GetRow(itemId);
-
- var info = new CurrencyStaticInfo
- {
- ItemId = itemId,
- IconId = item.Icon,
- MaxAmount = item.StackSize,
- };
-
- CurrencyStaticByItemIdCache[itemId] = info;
- return info;
- }
+ => InventoryScanner.GetEmptyItemSlotsString();
public static IReadOnlyList GetCurrencyInfoList(uint[] currencyIds)
- {
- if (currencyIds.Length == 0) return Array.Empty();
+ => CurrencyState.GetCurrencyInfoList(currencyIds);
- InventoryManager* inventoryManager = InventoryManager.Instance();
- if (inventoryManager == null) return Array.Empty();
+ public static void InvalidateCurrencyCaches()
+ => CurrencyState.InvalidateCaches();
- List currencyInfoList = new List(currencyIds.Length);
-
- for (int i = 0; i < currencyIds.Length; i++)
- {
- CurrencyItem currencyItem = ResolveCurrencyItemIdCached(currencyIds[i]);
- if (currencyItem.ItemId == 0)
- continue;
-
- CurrencyStaticInfo staticInfo = GetCurrencyStaticInfoCached(currencyItem.ItemId);
-
- uint amount = (uint)inventoryManager->GetInventoryItemCount(currencyItem.ItemId);
-
- bool isCapped = false;
- if (currencyItem.IsLimited)
- {
- int weeklyLimit = InventoryManager.GetLimitedTomestoneWeeklyLimit();
- int weeklyAcquired = inventoryManager->GetWeeklyAcquiredTomestoneCount();
- isCapped = weeklyAcquired >= weeklyLimit;
- }
-
- currencyInfoList.Add(new CurrencyInfo
- {
- Amount = amount,
- MaxAmount = staticInfo.MaxAmount,
- ItemId = staticInfo.ItemId,
- IconId = staticInfo.IconId,
- LimitReached = amount >= staticInfo.MaxAmount,
- IsCapped = isCapped
- });
- }
-
- return currencyInfoList;
- }
+ public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
+ => InventoryScanner.GetInventoryContainer(inventoryType);
private static void ClearAll()
{
@@ -595,86 +123,6 @@ public static unsafe class InventoryState
AllCategories.Clear();
FilteredCategories.Clear();
RemoveKeysScratch.Clear();
+ ClaimedKeys.Clear();
}
-
- private static CategoryInfo GetCategoryInfoForKeyCached(uint key, ItemInfo sample)
- {
- if (CategoryInfoCache.TryGetValue(key, out var cached))
- return cached;
-
- CategoryInfo info = GetCategoryInfoForKeySlow(key, sample);
- CategoryInfoCache[key] = info;
- return info;
- }
-
- private static CategoryInfo GetCategoryInfoForKeySlow(uint key, ItemInfo sample)
- {
- if (key == 0)
- {
- return new CategoryInfo
- {
- Name = "Misc",
- Description = "Uncategorized items",
- };
- }
-
- var uiCat = sample.UiCategory.Value;
- string? name = uiCat.Name.ToString();
-
- if (string.IsNullOrWhiteSpace(name))
- name = $"Category\\ {key}";
-
- return new CategoryInfo
- {
- Name = name,
- };
- }
-
- public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
- {
- return InventoryManager.Instance()->GetInventoryContainer(inventoryType);
- }
-
- private struct AggregatedItem
- {
- public InventoryItem First;
- public int Total;
- }
-
- private sealed class ItemCountDescComparer : IComparer
- {
- public static readonly ItemCountDescComparer Instance = new();
-
- public int Compare(ItemInfo? x, ItemInfo? y)
- {
- if (ReferenceEquals(x, y)) return 0;
- if (x is null) return 1;
- if (y is null) return -1;
-
- int a = x.ItemCount;
- int b = y.ItemCount;
-
- if (a > b) return -1;
- if (a < b) return 1;
- return 0;
- }
- }
-
- private sealed class CategoryBucket
- {
- public uint Key;
- public CategoryInfo Category = null!;
- public List Items = null!;
- public List FilteredItems = null!;
- public bool Used;
- }
-
- private struct CurrencyStaticInfo
- {
- public uint ItemId;
- public uint IconId;
- public uint MaxAmount;
- }
-
- private record CurrencyItem(uint ItemId, bool IsLimited);
-}
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/CurrencyListNode.cs b/AetherBags/Nodes/Currency/CurrencyListNode.cs
similarity index 84%
rename from AetherBags/Nodes/CurrencyListNode.cs
rename to AetherBags/Nodes/Currency/CurrencyListNode.cs
index c49f338..f4fc8bd 100644
--- a/AetherBags/Nodes/CurrencyListNode.cs
+++ b/AetherBags/Nodes/Currency/CurrencyListNode.cs
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using AetherBags.Currency;
using KamiToolKit.Nodes;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Currency;
public class CurrencyListNode : HorizontalListNode
{
diff --git a/AetherBags/Nodes/CurrencyNode.cs b/AetherBags/Nodes/Currency/CurrencyNode.cs
similarity index 96%
rename from AetherBags/Nodes/CurrencyNode.cs
rename to AetherBags/Nodes/Currency/CurrencyNode.cs
index 915ad98..a3adf98 100644
--- a/AetherBags/Nodes/CurrencyNode.cs
+++ b/AetherBags/Nodes/Currency/CurrencyNode.cs
@@ -4,9 +4,8 @@ using AetherBags.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
-using Lumina.Excel.Sheets;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Currency;
public class CurrencyNode : SimpleComponentNode
{
diff --git a/AetherBags/Nodes/FlexGrowDirection.cs b/AetherBags/Nodes/FlexGrowDirection.cs
deleted file mode 100644
index d3f94a3..0000000
--- a/AetherBags/Nodes/FlexGrowDirection.cs
+++ /dev/null
@@ -1,13 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace AetherBags.Nodes;
-
-public enum FlexGrowDirection
-{
- DownRight,
- DownLeft,
- UpRight,
- UpLeft
-}
diff --git a/AetherBags/Nodes/LabeledDropdownNode.cs b/AetherBags/Nodes/Input/LabeledDropdownNode.cs
similarity index 100%
rename from AetherBags/Nodes/LabeledDropdownNode.cs
rename to AetherBags/Nodes/Input/LabeledDropdownNode.cs
diff --git a/AetherBags/Nodes/TextInputWithHintNode.cs b/AetherBags/Nodes/Input/TextInputWithHintNode.cs
similarity index 54%
rename from AetherBags/Nodes/TextInputWithHintNode.cs
rename to AetherBags/Nodes/Input/TextInputWithHintNode.cs
index e29434a..5b7af51 100644
--- a/AetherBags/Nodes/TextInputWithHintNode.cs
+++ b/AetherBags/Nodes/Input/TextInputWithHintNode.cs
@@ -4,19 +4,19 @@ using KamiToolKit.Nodes;
using Lumina.Text;
using Lumina.Text.ReadOnly;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Input;
public class TextInputWithHintNode : SimpleComponentNode {
- private readonly TextInputNode textInputNode;
- private readonly ImageNode helpNode;
+ private readonly TextInputNode _textInputNode;
+ private readonly ImageNode _helpNode;
public TextInputWithHintNode() {
- textInputNode = new TextInputNode {
+ _textInputNode = new TextInputNode {
PlaceholderString = "Search . . .",
};
- textInputNode.AttachNode(this);
+ _textInputNode.AttachNode(this);
- helpNode = new SimpleImageNode {
+ _helpNode = new SimpleImageNode {
TexturePath = "ui/uld/CircleButtons.tex",
TextureCoordinates = new Vector2(112.0f, 84.0f),
TextureSize = new Vector2(28.0f, 28.0f),
@@ -26,26 +26,26 @@ public class TextInputWithHintNode : SimpleComponentNode {
.Append("Start input with '$' to search by description")
.ToReadOnlySeString(),
};
- helpNode.AttachNode(this);
+ _helpNode.AttachNode(this);
}
public required Action? OnInputReceived {
- get => textInputNode.OnInputReceived;
- set => textInputNode.OnInputReceived = value;
+ get => _textInputNode.OnInputReceived;
+ set => _textInputNode.OnInputReceived = value;
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
- helpNode.Size = new Vector2(Height, Height);
- helpNode.Position = new Vector2(Width - helpNode.Width - 5.0f, 0.0f);
+ _helpNode.Size = new Vector2(Height, Height);
+ _helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f);
- textInputNode.Size = new Vector2(Width - helpNode.Width - 5.0f, Height);
- textInputNode.Position = new Vector2(0.0f, 0.0f);
+ _textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height);
+ _textInputNode.Position = new Vector2(0.0f, 0.0f);
}
public ReadOnlySeString SearchString {
- get => textInputNode.SeString;
- set => textInputNode.SeString = value;
+ get => _textInputNode.SeString;
+ set => _textInputNode.SeString = value;
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/InventoryCategoryHoverCoordinator.cs b/AetherBags/Nodes/Inventory/InventoryCategoryHoverCoordinator.cs
similarity index 96%
rename from AetherBags/Nodes/InventoryCategoryHoverCoordinator.cs
rename to AetherBags/Nodes/Inventory/InventoryCategoryHoverCoordinator.cs
index f9054db..44cf1ed 100644
--- a/AetherBags/Nodes/InventoryCategoryHoverCoordinator.cs
+++ b/AetherBags/Nodes/Inventory/InventoryCategoryHoverCoordinator.cs
@@ -1,6 +1,6 @@
-using System;
+using AetherBags.Nodes.Layout;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Inventory;
public sealed class InventoryCategoryHoverCoordinator
{
diff --git a/AetherBags/Nodes/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
similarity index 99%
rename from AetherBags/Nodes/InventoryCategoryNode.cs
rename to AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
index f1a0fdf..be9c151 100644
--- a/AetherBags/Nodes/InventoryCategoryNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
@@ -1,20 +1,18 @@
-using AetherBags.Extensions;
-using AetherBags.Inventory;
-using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit;
-using KamiToolKit.Classes;
-using KamiToolKit.Nodes;
using System;
using System.Numerics;
+using AetherBags.Inventory;
+using AetherBags.Nodes.Layout;
+using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
-
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
// TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode
{
diff --git a/AetherBags/Nodes/InventoryDragDropNode.cs b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
similarity index 97%
rename from AetherBags/Nodes/InventoryDragDropNode.cs
rename to AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
index cc112a3..29b042a 100644
--- a/AetherBags/Nodes/InventoryDragDropNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
@@ -1,16 +1,14 @@
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;
using KamiToolKit.Nodes;
-
// TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Inventory;
public class InventoryDragDropNode : DragDropFixedNode
{
diff --git a/AetherBags/Nodes/InventoryFooterNode.cs b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs
similarity index 95%
rename from AetherBags/Nodes/InventoryFooterNode.cs
rename to AetherBags/Nodes/Inventory/InventoryFooterNode.cs
index 11a4095..27a9105 100644
--- a/AetherBags/Nodes/InventoryFooterNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs
@@ -1,15 +1,13 @@
-using System;
using System.Collections.Generic;
using System.Numerics;
using AetherBags.Currency;
using AetherBags.Inventory;
-using FFXIVClientStructs.FFXIV.Client.Game;
+using AetherBags.Nodes.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
-using Lumina.Excel.Sheets;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Inventory;
public sealed class InventoryFooterNode : SimpleComponentNode
{
diff --git a/AetherBags/Nodes/Layout/FlexGrowDirection.cs b/AetherBags/Nodes/Layout/FlexGrowDirection.cs
new file mode 100644
index 0000000..e4bec85
--- /dev/null
+++ b/AetherBags/Nodes/Layout/FlexGrowDirection.cs
@@ -0,0 +1,9 @@
+namespace AetherBags.Nodes.Layout;
+
+public enum FlexGrowDirection
+{
+ DownRight,
+ DownLeft,
+ UpRight,
+ UpLeft
+}
diff --git a/AetherBags/Nodes/HybridDirectionalFlexNode.cs b/AetherBags/Nodes/Layout/HybridDirectionalFlexNode.cs
similarity index 97%
rename from AetherBags/Nodes/HybridDirectionalFlexNode.cs
rename to AetherBags/Nodes/Layout/HybridDirectionalFlexNode.cs
index c92a041..fc9b49d 100644
--- a/AetherBags/Nodes/HybridDirectionalFlexNode.cs
+++ b/AetherBags/Nodes/Layout/HybridDirectionalFlexNode.cs
@@ -1,9 +1,7 @@
using KamiToolKit;
using KamiToolKit.Nodes;
-using System;
-using System.Runtime.CompilerServices;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Layout;
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode { }
diff --git a/AetherBags/Nodes/HybridDirectionalStackNode.cs b/AetherBags/Nodes/Layout/HybridDirectionalStackNode.cs
similarity index 98%
rename from AetherBags/Nodes/HybridDirectionalStackNode.cs
rename to AetherBags/Nodes/Layout/HybridDirectionalStackNode.cs
index dc051b2..df5138e 100644
--- a/AetherBags/Nodes/HybridDirectionalStackNode.cs
+++ b/AetherBags/Nodes/Layout/HybridDirectionalStackNode.cs
@@ -1,8 +1,7 @@
-using System;
using KamiToolKit;
using KamiToolKit.Nodes;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Layout;
public class HybridDirectionalStackNode : LayoutListNode where T : NodeBase
{
diff --git a/AetherBags/Nodes/WrappingGridNode.cs b/AetherBags/Nodes/Layout/WrappingGridNode.cs
similarity index 99%
rename from AetherBags/Nodes/WrappingGridNode.cs
rename to AetherBags/Nodes/Layout/WrappingGridNode.cs
index 4b21e45..385c503 100644
--- a/AetherBags/Nodes/WrappingGridNode.cs
+++ b/AetherBags/Nodes/Layout/WrappingGridNode.cs
@@ -1,11 +1,11 @@
-using KamiToolKit;
-using KamiToolKit.Nodes;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
+using KamiToolKit;
+using KamiToolKit.Nodes;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Layout;
public sealed class WrappingGridNode : LayoutListNode where T : NodeBase
{
diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs
index 2c83b1c..858226a 100644
--- a/AetherBags/Plugin.cs
+++ b/AetherBags/Plugin.cs
@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using AetherBags.Addons;
using AetherBags.Helpers;
+using AetherBags.Hooks;
using Dalamud.Plugin;
using Dalamud.Game.Command;
using Dalamud.Hooking;
@@ -13,6 +14,9 @@ namespace AetherBags;
public unsafe class Plugin : IDalamudPlugin
{
private static string HelpDescription => "Opens your inventory.";
+
+ private readonly InventoryHooks _inventoryHooks;
+
public Plugin(IDalamudPluginInterface pluginInterface)
{
pluginInterface.Create();
@@ -59,31 +63,7 @@ public unsafe class Plugin : IDalamudPlugin
Services.Framework.RunOnFrameworkThread(OnLogin);
}
- try
- {
- _moveItemSlotHook = Services.GameInteropProvider.HookFromSignature("E8 ?? ?? ?? ?? 48 8B 03 66 FF C5", MoveItemSlotDetour);
- _moveItemSlotHook.Enable();
-
- Services.Logger.Debug("MoveItemSlot hooked successfully.");
- }
- catch (Exception e)
- {
- Services.Logger.Error(e, "Failed to hook MoveItemSlot");
- }
- }
-
- private unsafe delegate int MoveItemSlotDelegate(InventoryManager* inventoryManager, InventoryType srcContainer, ushort srcSlot, InventoryType dstContainer, ushort dstSlot, bool unk);
-
- private Hook? _moveItemSlotHook;
-
- private unsafe int MoveItemSlotDetour(InventoryManager* manager, InventoryType srcType, ushort srcSlot, InventoryType dstType, ushort dstSlot, bool unk)
- {
- InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
- InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
- Services.Logger.Info($"[MoveItemSlot] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
-
- // Call the original function
- return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
+ _inventoryHooks = new InventoryHooks();
}
public void Dispose()
@@ -101,7 +81,7 @@ public unsafe class Plugin : IDalamudPlugin
KamiToolKitLibrary.Dispose();
- _moveItemSlotHook?.Dispose();
+ _inventoryHooks.Dispose();
}
private void OnCommand(string command, string args)