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