Slight refactor/moving namespaces

This commit is contained in:
Zeffuro
2025-12-24 19:50:11 +01:00
parent 9dfa0ec7aa
commit a0fb7f5103
24 changed files with 855 additions and 687 deletions
@@ -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;
+149
View File
@@ -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;
/// <summary>
/// Manages currency lookups, caching, and retrieval from the game.
/// </summary>
public static unsafe class CurrencyState
{
private const uint CurrencyIdLimitedTomestone = 0xFFFF_FFFE;
private const uint CurrencyIdNonLimitedTomestone = 0xFFFF_FFFD;
private static readonly Dictionary<uint, CurrencyItem> CurrencyItemByCurrencyIdCache = new(capacity: 32);
private static readonly Dictionary<uint, CurrencyStaticInfo> 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<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
{
if (currencyIds.Length == 0)
return Array.Empty<CurrencyInfo>();
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
return Array.Empty<CurrencyInfo>();
List<CurrencyInfo> currencyInfoList = new List<CurrencyInfo>(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<TomestonesItem>()
.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<TomestonesItem>()
.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<Item>().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);
}
@@ -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,
+4 -4
View File
@@ -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<SystemConfiguration>(file.FullName);
FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
return JsonFileHelper.LoadFile<SystemConfiguration>(file.FullName);
}
public static SystemConfiguration LoadConfigOrDefault()
+60
View File
@@ -0,0 +1,60 @@
using System;
using Dalamud.Hooking;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Hooks;
/// <summary>
/// Manages hooks related to inventory operations.
/// </summary>
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<MoveItemSlotDelegate>? _moveItemSlotHook;
public InventoryHooks()
{
try
{
_moveItemSlotHook = Services.GameInteropProvider.HookFromSignature<MoveItemSlotDelegate>(
"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();
}
}
@@ -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<uint, CategoryInfo> CategoryInfoCache = new(capacity: 256);
public static uint MakeUserCategoryKey(int order)
=> UserCategoryKeyFlag | (uint)(order & 0x7FFF_FFFF);
public static bool IsUserCategoryKey(uint key)
=> (key & UserCategoryKeyFlag) != 0;
/// <summary>
/// Resets all buckets for a new refresh cycle.
/// </summary>
public static void ResetBuckets(Dictionary<uint, CategoryBucket> bucketsByKey)
{
foreach (var kvp in bucketsByKey)
{
CategoryBucket bucket = kvp.Value;
bucket.Used = false;
bucket.Items.Clear();
bucket.FilteredItems.Clear();
}
}
public static void BucketByUserCategories(
Dictionary<ulong, ItemInfo> itemInfoByKey,
List<UserCategoryDefinition> userCategories,
Dictionary<uint, CategoryBucket> bucketsByKey,
HashSet<ulong> claimedKeys,
List<UserCategoryDefinition> 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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey,
HashSet<ulong> 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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(capacity: 16),
Used = true,
};
bucketsByKey.Add(categoryKey, bucket);
}
else
{
bucket.Used = true;
}
bucket.Items.Add(info);
}
}
public static void BucketUnclaimedToMisc(
Dictionary<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey,
HashSet<ulong> 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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<uint, CategoryBucket> bucketsByKey,
List<uint> 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<uint, CategoryBucket> bucketsByKey,
List<uint> sortedCategoryKeys,
List<CategorizedInventory> 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<ItemInfo> Items = null!;
public List<ItemInfo> FilteredItems = null!;
public bool Used;
}
public sealed class ItemCountDescComparer : IComparer<ItemInfo>
{
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;
}
}
+66
View File
@@ -0,0 +1,66 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace AetherBags.Inventory;
public static class InventoryFilter
{
public static IReadOnlyList<CategorizedInventory> FilterCategories(
IReadOnlyList<CategorizedInventory> allCategories,
Dictionary<uint, CategoryBucket> bucketsByKey,
List<CategorizedInventory> 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;
}
}
+179
View File
@@ -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<ulong, AggregatedItem> 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<ulong, AggregatedItem> aggByKey,
Dictionary<ulong, ItemInfo> 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<ulong, AggregatedItem> aggByKey,
Dictionary<ulong, ItemInfo> itemInfoByKey,
List<ulong> 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;
}
+47 -599
View File
@@ -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<uint, CategoryInfo> CategoryInfoCache = new(capacity: 256);
public static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
private static readonly Dictionary<uint, CategoryBucket> BucketsByKey = new(capacity: 256);
private static readonly List<uint> SortedCategoryKeys = new(capacity: 256);
private static readonly List<CategorizedInventory> AllCategories = new(capacity: 256);
private static readonly List<CategorizedInventory> FilteredCategories = new(capacity: 256);
private static readonly List<UserCategoryDefinition> UserCategoriesSortedScratch = new(capacity: 64);
private static readonly List<ulong> 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<ulong> RemoveKeysScratch = new(capacity: 256);
private static readonly HashSet<ulong> ClaimedKeys = new(capacity: 512);
public static bool Contains(this IReadOnlyCollection<InventoryType> 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<UserCategoryDefinition> 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<ulong> claimedKeys = new HashSet<ulong>(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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<CategorizedInventory> 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<uint, CurrencyItem> CurrencyItemByCurrencyIdCache = new(capacity: 32);
private static readonly Dictionary<uint, CurrencyStaticInfo> 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<TomestonesItem>()
.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<TomestonesItem>()
.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<Item>().GetRow(itemId);
var info = new CurrencyStaticInfo
{
ItemId = itemId,
IconId = item.Icon,
MaxAmount = item.StackSize,
};
CurrencyStaticByItemIdCache[itemId] = info;
return info;
}
=> InventoryScanner.GetEmptyItemSlotsString();
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
{
if (currencyIds.Length == 0) return Array.Empty<CurrencyInfo>();
=> CurrencyState.GetCurrencyInfoList(currencyIds);
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null) return Array.Empty<CurrencyInfo>();
public static void InvalidateCurrencyCaches()
=> CurrencyState.InvalidateCaches();
List<CurrencyInfo> currencyInfoList = new List<CurrencyInfo>(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<ItemInfo>
{
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<ItemInfo> Items = null!;
public List<ItemInfo> FilteredItems = null!;
public bool Used;
}
private struct CurrencyStaticInfo
{
public uint ItemId;
public uint IconId;
public uint MaxAmount;
}
private record CurrencyItem(uint ItemId, bool IsLimited);
}
}
@@ -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
{
@@ -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
{
-13
View File
@@ -1,13 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace AetherBags.Nodes;
public enum FlexGrowDirection
{
DownRight,
DownLeft,
UpRight,
UpLeft
}
@@ -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<ReadOnlySeString>? 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;
}
}
@@ -1,6 +1,6 @@
using System;
using AetherBags.Nodes.Layout;
namespace AetherBags.Nodes;
namespace AetherBags.Nodes.Inventory;
public sealed class InventoryCategoryHoverCoordinator
{
@@ -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
{
@@ -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
{
@@ -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
{
@@ -0,0 +1,9 @@
namespace AetherBags.Nodes.Layout;
public enum FlexGrowDirection
{
DownRight,
DownLeft,
UpRight,
UpLeft
}
@@ -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<NodeBase> { }
@@ -1,8 +1,7 @@
using System;
using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes;
namespace AetherBags.Nodes.Layout;
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
{
@@ -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<T> : LayoutListNode where T : NodeBase
{
+6 -26
View File
@@ -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<Services>();
@@ -59,31 +63,7 @@ public unsafe class Plugin : IDalamudPlugin
Services.Framework.RunOnFrameworkThread(OnLogin);
}
try
{
_moveItemSlotHook = Services.GameInteropProvider.HookFromSignature<MoveItemSlotDelegate>("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<MoveItemSlotDelegate>? _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)