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)