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.Extensions;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Nodes; 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;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; 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; namespace AetherBags.Helpers;
public static class FileHelpers { public static class JsonFileHelper {
private static readonly JsonSerializerOptions SerializerOptions = new() { private static readonly JsonSerializerOptions SerializerOptions = new() {
WriteIndented = true, WriteIndented = true,
IncludeFields = true, IncludeFields = true,
+4 -4
View File
@@ -80,14 +80,14 @@ public static class Util
public static void SaveConfig(SystemConfiguration config) public static void SaveConfig(SystemConfiguration config)
{ {
FileInfo file = FileHelpers.GetFileInfo(SystemConfiguration.FileName); FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
FileHelpers.SaveFile(config, file.FullName); JsonFileHelper.SaveFile(config, file.FullName);
} }
private static SystemConfiguration LoadConfig() private static SystemConfiguration LoadConfig()
{ {
FileInfo file = FileHelpers.GetFileInfo(SystemConfiguration.FileName); FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
return FileHelpers.LoadFile<SystemConfiguration>(file.FullName); return JsonFileHelper.LoadFile<SystemConfiguration>(file.FullName);
} }
public static SystemConfiguration LoadConfigOrDefault() 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.Configuration;
using AetherBags.Currency;
using Dalamud.Game.Inventory; using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel.Sheets;
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using AetherBags.Currency;
using CurrencyManager = FFXIVClientStructs.FFXIV.Client.Game.CurrencyManager;
namespace AetherBags.Inventory; namespace AetherBags.Inventory;
public static unsafe class InventoryState public static unsafe class InventoryState
{ {
public static readonly InventoryType[] StandardInventories = public static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.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);
private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512); private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = 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 Dictionary<uint, CategoryBucket> BucketsByKey = new(capacity: 256);
private static readonly List<uint> SortedCategoryKeys = 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> AllCategories = new(capacity: 256);
private static readonly List<CategorizedInventory> FilteredCategories = 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<UserCategoryDefinition> UserCategoriesSortedScratch = new(capacity: 64);
private static readonly List<ulong> RemoveKeysScratch = new(capacity: 256); private static readonly List<ulong> RemoveKeysScratch = new(capacity: 256);
private static readonly HashSet<ulong> ClaimedKeys = new(capacity: 512);
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);
public static bool Contains(this IReadOnlyCollection<InventoryType> inventoryTypes, GameInventoryType type) public static bool Contains(this IReadOnlyCollection<InventoryType> inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type); => inventoryTypes.Contains((InventoryType)type);
private static ulong MakeNaturalSlotKey(InventoryType container, int slot)
=> ((ulong)(uint)container << 32) | (uint)slot;
public static void RefreshFromGame() public static void RefreshFromGame()
{ {
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
@@ -87,497 +35,77 @@ public static unsafe class InventoryState
} }
var config = System.Config; var config = System.Config;
InventoryStackMode stackMode = config.General.StackMode; InventoryStackMode stackMode = config.General.StackMode;
bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled; bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled;
bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled; bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled;
List<UserCategoryDefinition> userCategories = config.Categories.UserCategories; List<UserCategoryDefinition> userCategories = config.Categories.UserCategories;
Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}");
AggByKey.Clear(); AggByKey.Clear();
ItemInfoByKey.Clear(); ItemInfoByKey.Clear();
BucketsByKey.Clear();
SortedCategoryKeys.Clear(); SortedCategoryKeys.Clear();
AllCategories.Clear(); AllCategories.Clear();
FilteredCategories.Clear(); FilteredCategories.Clear();
ClaimedKeys.Clear();
Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}"); InventoryScanner.ScanBags(inventoryManager, stackMode, AggByKey);
CategoryBucketManager.ResetBuckets(BucketsByKey);
int scannedSlots = 0; InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey);
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);
if (userCategoriesEnabled && userCategories.Count > 0) if (userCategoriesEnabled && userCategories.Count > 0)
{ {
UserCategoriesSortedScratch.Clear(); CategoryBucketManager.BucketByUserCategories(
UserCategoriesSortedScratch.AddRange(userCategories); ItemInfoByKey,
UserCategoriesSortedScratch.Sort((a, b) => userCategories,
{ BucketsByKey,
int p = a.Priority.CompareTo(b.Priority); ClaimedKeys,
if (p != 0) return p; UserCategoriesSortedScratch);
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;
}
} }
// Game category bucket
if (gameCategoriesEnabled) if (gameCategoriesEnabled)
{ {
foreach (var itemKvp in ItemInfoByKey) CategoryBucketManager.BucketByGameCategories(
{ ItemInfoByKey,
ulong itemKey = itemKvp.Key; BucketsByKey,
ItemInfo info = itemKvp.Value; ClaimedKeys,
userCategoriesEnabled);
if (userCategoriesEnabled && claimedKeys.Contains(itemKey)) }
continue; else
{
uint categoryKey = info.UiCategory.RowId; CategoryBucketManager.BucketUnclaimedToMisc(
ItemInfoByKey,
if (!BucketsByKey.TryGetValue(categoryKey, out CategoryBucket? bucket)) BucketsByKey,
{ ClaimedKeys,
bucket = new CategoryBucket userCategoriesEnabled);
{
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);
}
} }
// Unclaimed items InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch);
if (!gameCategoriesEnabled) CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys);
{ CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories);
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}");
} }
public static IReadOnlyList<CategorizedInventory> GetInventoryItemCategories(string filterString = "", bool invert = false) public static IReadOnlyList<CategorizedInventory> GetInventoryItemCategories(string filterString = "", bool invert = false)
{ {
if (string.IsNullOrEmpty(filterString)) return InventoryFilter.FilterCategories(
return AllCategories; AllCategories,
BucketsByKey,
Regex? re = null; FilteredCategories,
bool regexValid = true; filterString,
invert);
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;
} }
public static string GetEmptyItemSlotsString() public static string GetEmptyItemSlotsString()
{ => InventoryScanner.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;
}
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds) public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
{ => CurrencyState.GetCurrencyInfoList(currencyIds);
if (currencyIds.Length == 0) return Array.Empty<CurrencyInfo>();
InventoryManager* inventoryManager = InventoryManager.Instance(); public static void InvalidateCurrencyCaches()
if (inventoryManager == null) return Array.Empty<CurrencyInfo>(); => CurrencyState.InvalidateCaches();
List<CurrencyInfo> currencyInfoList = new List<CurrencyInfo>(currencyIds.Length); public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> InventoryScanner.GetInventoryContainer(inventoryType);
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 void ClearAll() private static void ClearAll()
{ {
@@ -595,86 +123,6 @@ public static unsafe class InventoryState
AllCategories.Clear(); AllCategories.Clear();
FilteredCategories.Clear(); FilteredCategories.Clear();
RemoveKeysScratch.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 AetherBags.Currency;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Currency;
public class CurrencyListNode : HorizontalListNode public class CurrencyListNode : HorizontalListNode
{ {
@@ -4,9 +4,8 @@ using AetherBags.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Currency;
public class CurrencyNode : SimpleComponentNode 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;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Input;
public class TextInputWithHintNode : SimpleComponentNode { public class TextInputWithHintNode : SimpleComponentNode {
private readonly TextInputNode textInputNode; private readonly TextInputNode _textInputNode;
private readonly ImageNode helpNode; private readonly ImageNode _helpNode;
public TextInputWithHintNode() { public TextInputWithHintNode() {
textInputNode = new TextInputNode { _textInputNode = new TextInputNode {
PlaceholderString = "Search . . .", PlaceholderString = "Search . . .",
}; };
textInputNode.AttachNode(this); _textInputNode.AttachNode(this);
helpNode = new SimpleImageNode { _helpNode = new SimpleImageNode {
TexturePath = "ui/uld/CircleButtons.tex", TexturePath = "ui/uld/CircleButtons.tex",
TextureCoordinates = new Vector2(112.0f, 84.0f), TextureCoordinates = new Vector2(112.0f, 84.0f),
TextureSize = new Vector2(28.0f, 28.0f), TextureSize = new Vector2(28.0f, 28.0f),
@@ -26,26 +26,26 @@ public class TextInputWithHintNode : SimpleComponentNode {
.Append("Start input with '$' to search by description") .Append("Start input with '$' to search by description")
.ToReadOnlySeString(), .ToReadOnlySeString(),
}; };
helpNode.AttachNode(this); _helpNode.AttachNode(this);
} }
public required Action<ReadOnlySeString>? OnInputReceived { public required Action<ReadOnlySeString>? OnInputReceived {
get => textInputNode.OnInputReceived; get => _textInputNode.OnInputReceived;
set => textInputNode.OnInputReceived = value; set => _textInputNode.OnInputReceived = value;
} }
protected override void OnSizeChanged() { protected override void OnSizeChanged() {
base.OnSizeChanged(); base.OnSizeChanged();
helpNode.Size = new Vector2(Height, Height); _helpNode.Size = new Vector2(Height, Height);
helpNode.Position = new Vector2(Width - helpNode.Width - 5.0f, 0.0f); _helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f);
textInputNode.Size = new Vector2(Width - helpNode.Width - 5.0f, Height); _textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height);
textInputNode.Position = new Vector2(0.0f, 0.0f); _textInputNode.Position = new Vector2(0.0f, 0.0f);
} }
public ReadOnlySeString SearchString { public ReadOnlySeString SearchString {
get => textInputNode.SeString; get => _textInputNode.SeString;
set => textInputNode.SeString = value; 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 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;
using System.Numerics; 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;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; 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 // TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode; using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType; using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode public class InventoryCategoryNode : SimpleComponentNode
{ {
@@ -1,16 +1,14 @@
using System.Numerics; using System.Numerics;
using AetherBags.Extensions;
using AetherBags.Inventory; using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
// TODO: Switch back to CS version when Dalamud Updated // TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode; using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Inventory;
public class InventoryDragDropNode : DragDropFixedNode public class InventoryDragDropNode : DragDropFixedNode
{ {
@@ -1,15 +1,13 @@
using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Currency; using AetherBags.Currency;
using AetherBags.Inventory; using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using AetherBags.Nodes.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using Lumina.Excel.Sheets;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Inventory;
public sealed class InventoryFooterNode : SimpleComponentNode 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;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using System;
using System.Runtime.CompilerServices;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Layout;
public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { } public class HybridDirectionalFlexNode : HybridDirectionalFlexNode<NodeBase> { }
@@ -1,8 +1,7 @@
using System;
using KamiToolKit; using KamiToolKit;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Layout;
public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase public class HybridDirectionalStackNode<T> : LayoutListNode where T : NodeBase
{ {
@@ -1,11 +1,11 @@
using KamiToolKit;
using KamiToolKit.Nodes;
using System; using System;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.Runtime.CompilerServices; 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 public sealed class WrappingGridNode<T> : LayoutListNode where T : NodeBase
{ {
+6 -26
View File
@@ -2,6 +2,7 @@ using System;
using System.Numerics; using System.Numerics;
using AetherBags.Addons; using AetherBags.Addons;
using AetherBags.Helpers; using AetherBags.Helpers;
using AetherBags.Hooks;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Hooking; using Dalamud.Hooking;
@@ -13,6 +14,9 @@ namespace AetherBags;
public unsafe class Plugin : IDalamudPlugin public unsafe class Plugin : IDalamudPlugin
{ {
private static string HelpDescription => "Opens your inventory."; private static string HelpDescription => "Opens your inventory.";
private readonly InventoryHooks _inventoryHooks;
public Plugin(IDalamudPluginInterface pluginInterface) public Plugin(IDalamudPluginInterface pluginInterface)
{ {
pluginInterface.Create<Services>(); pluginInterface.Create<Services>();
@@ -59,31 +63,7 @@ public unsafe class Plugin : IDalamudPlugin
Services.Framework.RunOnFrameworkThread(OnLogin); Services.Framework.RunOnFrameworkThread(OnLogin);
} }
try _inventoryHooks = new InventoryHooks();
{
_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);
} }
public void Dispose() public void Dispose()
@@ -101,7 +81,7 @@ public unsafe class Plugin : IDalamudPlugin
KamiToolKitLibrary.Dispose(); KamiToolKitLibrary.Dispose();
_moveItemSlotHook?.Dispose(); _inventoryHooks.Dispose();
} }
private void OnCommand(string command, string args) private void OnCommand(string command, string args)