Slight refactor/moving namespaces
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
+1
-1
@@ -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
|
||||||
{
|
{
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace AetherBags.Nodes;
|
|
||||||
|
|
||||||
public enum FlexGrowDirection
|
|
||||||
{
|
|
||||||
DownRight,
|
|
||||||
DownLeft,
|
|
||||||
UpRight,
|
|
||||||
UpLeft
|
|
||||||
}
|
|
||||||
+15
-15
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+2
-2
@@ -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
|
||||||
{
|
{
|
||||||
+7
-9
@@ -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
-3
@@ -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
|
||||||
{
|
{
|
||||||
+2
-4
@@ -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
-3
@@ -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
-2
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user