Implement VERY basic configuration window and stacking

This commit is contained in:
Zeffuro
2025-12-22 16:34:01 +01:00
parent fec004ffce
commit fff0578b74
16 changed files with 363 additions and 52 deletions
+2
View File
@@ -1,3 +1,5 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation"> <wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACurrencyManager_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe66db7cd515142b9bbfb1b4e18f82ace825448_003Ffc_003F78df30c7_003FCurrencyManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInventoryManager_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d322ffbe41aca452171c1858ac4d72a967922191dfb8ada66667df5fd58b_003FInventoryManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AItem_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F8ec7cc8a18dbb6a6f3c21f8adcb4e2661dc7979_003FItem_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary> <s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AItem_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F8ec7cc8a18dbb6a6f3c21f8adcb4e2661dc7979_003FItem_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using AetherBags.Nodes.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Addons;
public class AddonConfigurationWindow : NativeAddon
{
private TabBarNode _tabBarNode = null!;
private GeneralScrollingAreaNode _generalScrollingAreaNode = null!;
private CategoryScrollingAreaNode _categoryScrollingAreaNode = null!;
private CurrencyScrollingAreaNode _currencyScrollingAreaNode = null!;
private readonly List<NodeBase> _tabContent = new();
protected override unsafe void OnSetup(AtkUnitBase* addon)
{
var tabContentY = ContentStartPosition.Y + 40;
var tabContentHeight = ContentSize.Y - 40;
_tabBarNode = new TabBarNode
{
Position = ContentStartPosition,
Size = ContentSize with { Y = 24 },
IsVisible = true
};
_tabBarNode.AttachNode(this);
_generalScrollingAreaNode = new GeneralScrollingAreaNode
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = true,
};
_generalScrollingAreaNode.AttachNode(this);
_categoryScrollingAreaNode = new CategoryScrollingAreaNode
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = false,
};
_categoryScrollingAreaNode.AttachNode(this);
_currencyScrollingAreaNode = new CurrencyScrollingAreaNode
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = false,
};
_currencyScrollingAreaNode.AttachNode(this);
_tabContent.Add(_generalScrollingAreaNode);
_tabContent.Add(_categoryScrollingAreaNode);
_tabContent.Add(_currencyScrollingAreaNode);
_tabBarNode.AddTab("General", () => SwitchTab(0));
_tabBarNode.AddTab("Categories", () => SwitchTab(1));
_tabBarNode.AddTab("Currency", () => SwitchTab(2));
base.OnSetup(addon);
}
private void SwitchTab(int index)
{
for (var i = 0; i < _tabContent.Count; i++)
_tabContent[i].IsVisible = i == index;
}
}
@@ -0,0 +1,16 @@
using System.Numerics;
using KamiToolKit.Classes;
namespace AetherBags.Configuration;
public class GeneralSettings
{
public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId;
public bool DebugEnabled { get; set; } = false;
}
public enum InventoryStackMode : byte
{
NaturalStacks = 0,
AggregateByItemId = 1,
}
@@ -7,6 +7,8 @@ public class SystemConfiguration
{ {
public const string FileName = "AetherBags.json"; public const string FileName = "AetherBags.json";
public CurrencySettings Currency { get; set; } = new();
public GeneralSettings General { get; set; } = new();
public CategorySettings Categories { get; set; } = new(); public CategorySettings Categories { get; set; } = new();
public CurrencySettings Currency { get; set; } = new();
} }
@@ -43,11 +43,11 @@ public static class AddonLifecycleExtensions {
private static void Logger(AddonEvent type, AddonArgs args) { private static void Logger(AddonEvent type, AddonArgs args) {
switch (args) { switch (args) {
case AddonReceiveEventArgs receiveEventArgs: case AddonReceiveEventArgs receiveEventArgs:
Services.Logger.Debug($"[{args.AddonName}] {(AtkEventType)receiveEventArgs.AtkEventType}: {receiveEventArgs.EventParam}"); Services.Logger.DebugOnly($"[{args.AddonName}] {(AtkEventType)receiveEventArgs.AtkEventType}: {receiveEventArgs.EventParam}");
break; break;
default: default:
Services.Logger.Debug($"{args.AddonName} called {type.ToString().Replace("Post", string.Empty)}"); Services.Logger.DebugOnly($"{args.AddonName} called {type.ToString().Replace("Post", string.Empty)}");
break; break;
} }
} }
+16
View File
@@ -0,0 +1,16 @@
using System.Diagnostics;
namespace AetherBags.Extensions;
public static class LoggerExtensions
{
public static void DebugOnly(this object logger, string message)
{
if(System.Config.General.DebugEnabled) Services.Logger.Debug(message);
}
public static void DebugOnly(this object logger, string message, params object[] args)
{
if(System.Config.General.DebugEnabled) Services.Logger.Debug(message);
}
}
+2 -1
View File
@@ -1 +1,2 @@
global using KamiToolKit.Extensions; global using KamiToolKit.Extensions;
global using AetherBags.Extensions;
+2 -2
View File
@@ -62,7 +62,7 @@ public static class BackupHelper {
File.Move(latestFile.FullName, archivePath); File.Move(latestFile.FullName, archivePath);
moved = true; moved = true;
} catch (IOException ioEx) when (i < 4) { } catch (IOException ioEx) when (i < 4) {
Services.Logger.Debug($"Move failed, retrying in 100ms: {ioEx.Message}"); Services.Logger.DebugOnly($"Move failed, retrying in 100ms: {ioEx.Message}");
global::System.Threading.Thread.Sleep(100); global::System.Threading.Thread.Sleep(100);
} }
} }
@@ -79,7 +79,7 @@ public static class BackupHelper {
var allBackups = dir.GetFiles().Where(f => f.Name.StartsWith($"{Name}.2") && f.Name.EndsWith(".zip")) var allBackups = dir.GetFiles().Where(f => f.Name.StartsWith($"{Name}.2") && f.Name.EndsWith(".zip"))
.OrderBy(f => f.LastWriteTime.Ticks).ToList(); .OrderBy(f => f.LastWriteTime.Ticks).ToList();
if (allBackups.Count > MaxBackups) { if (allBackups.Count > MaxBackups) {
Services.Logger.Debug($"Removing Oldest Backup: {allBackups[0].FullName}"); Services.Logger.DebugOnly($"Removing Oldest Backup: {allBackups[0].FullName}");
File.Delete(allBackups[0].FullName); File.Delete(allBackups[0].FullName);
} }
} catch (Exception exception) { } catch (Exception exception) {
+90 -43
View File
@@ -1,3 +1,4 @@
using AetherBags.Configuration;
using AetherBags.Currency; using AetherBags.Currency;
using Dalamud.Game.Inventory; using Dalamud.Game.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
@@ -6,7 +7,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AetherBags.Configuration;
namespace AetherBags.Inventory; namespace AetherBags.Inventory;
@@ -46,19 +46,17 @@ public static unsafe class InventoryState
private static readonly Dictionary<uint, CategoryInfo> CategoryInfoCache = new(capacity: 256); private static readonly Dictionary<uint, CategoryInfo> CategoryInfoCache = new(capacity: 256);
private static readonly Dictionary<uint, AggregatedItem> AggByItemId = new(capacity: 512); private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<uint, ItemInfo> ItemInfoByItemId = 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<uint> RemoveKeysScratch = new(capacity: 256);
private const uint UserCategoryKeyFlag = 0x8000_0000; private const uint UserCategoryKeyFlag = 0x8000_0000;
@@ -71,6 +69,9 @@ public static unsafe class InventoryState
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();
@@ -82,41 +83,78 @@ public static unsafe class InventoryState
var config = System.Config; var config = System.Config;
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;
AggByItemId.Clear(); AggByKey.Clear();
ItemInfoByKey.Clear();
BucketsByKey.Clear();
SortedCategoryKeys.Clear();
AllCategories.Clear();
FilteredCategories.Clear();
Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}");
int scannedSlots = 0;
int nonEmptySlots = 0;
int collisions = 0;
for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++) for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
{ {
var container = inventoryManager->GetInventoryContainer(BagInventories[inventoryIndex]); var inventoryType = BagInventories[inventoryIndex];
var container = inventoryManager->GetInventoryContainer(inventoryType);
if (container == null) if (container == null)
{
Services.Logger.DebugOnly($"Container null: {inventoryType}");
continue; continue;
}
int size = container->Size; int size = container->Size;
Services.Logger.DebugOnly($"Scanning {inventoryType} Size={size}");
for (int slot = 0; slot < size; slot++) for (int slot = 0; slot < size; slot++)
{ {
scannedSlots++;
ref var item = ref container->Items[slot]; ref var item = ref container->Items[slot];
uint id = item.ItemId; uint id = item.ItemId;
if (id == 0) if (id == 0)
continue; continue;
nonEmptySlots++;
int quantity = item.Quantity; int quantity = item.Quantity;
if (AggByItemId.TryGetValue(id, out AggregatedItem agg)) ulong key = stackMode == InventoryStackMode.AggregateByItemId
? id
: 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; agg.Total += quantity;
AggByItemId[id] = agg; AggByKey[key] = agg;
} }
else else
{ {
AggByItemId.Add(id, new AggregatedItem { First = item, Total = quantity }); 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) foreach (var kvp in BucketsByKey)
{ {
CategoryBucket bucket = kvp.Value; CategoryBucket bucket = kvp.Value;
@@ -125,19 +163,20 @@ public static unsafe class InventoryState
bucket.FilteredItems.Clear(); bucket.FilteredItems.Clear();
} }
foreach (var kvp in AggByItemId) foreach (var kvp in AggByKey)
{ {
uint itemId = kvp.Key; ulong key = kvp.Key;
AggregatedItem agg = kvp.Value; AggregatedItem agg = kvp.Value;
if (!ItemInfoByItemId.TryGetValue(itemId, out ItemInfo? info)) if (!ItemInfoByKey.TryGetValue(key, out ItemInfo? info))
{ {
info = new ItemInfo info = new ItemInfo
{ {
Key = key,
Item = agg.First, Item = agg.First,
ItemCount = agg.Total, ItemCount = agg.Total,
}; };
ItemInfoByItemId.Add(itemId, info); ItemInfoByKey.Add(key, info);
} }
else else
{ {
@@ -146,8 +185,10 @@ public static unsafe class InventoryState
} }
} }
Services.Logger.DebugOnly($"ItemInfoByKey.Count={ItemInfoByKey.Count}");
// Bucket by user category // Bucket by user category
HashSet<uint> claimedItemIds = new(capacity: ItemInfoByItemId.Count); HashSet<ulong> claimedKeys = new HashSet<ulong>(capacity: ItemInfoByKey.Count);
if (userCategoriesEnabled && userCategories.Count > 0) if (userCategoriesEnabled && userCategories.Count > 0)
{ {
@@ -167,13 +208,13 @@ public static unsafe class InventoryState
for (int c = 0; c < UserCategoriesSortedScratch.Count; c++) for (int c = 0; c < UserCategoriesSortedScratch.Count; c++)
{ {
UserCategoryDefinition category = UserCategoriesSortedScratch[c]; UserCategoryDefinition category = UserCategoriesSortedScratch[c];
uint key = MakeUserCategoryKey(category.Order); uint bucketKey = MakeUserCategoryKey(category.Order);
if (!BucketsByKey.TryGetValue(key, out CategoryBucket? bucket)) if (!BucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
{ {
bucket = new CategoryBucket bucket = new CategoryBucket
{ {
Key = key, Key = bucketKey,
Category = new CategoryInfo Category = new CategoryInfo
{ {
Name = category.Name, Name = category.Name,
@@ -184,7 +225,7 @@ public static unsafe class InventoryState
FilteredItems = new List<ItemInfo>(capacity: 16), FilteredItems = new List<ItemInfo>(capacity: 16),
Used = true, Used = true,
}; };
BucketsByKey.Add(key, bucket); BucketsByKey.Add(bucketKey, bucket);
} }
else else
{ {
@@ -194,18 +235,18 @@ public static unsafe class InventoryState
bucket.Category.Color = category.Color; bucket.Category.Color = category.Color;
} }
foreach (var itemKvp in ItemInfoByItemId) foreach (var itemKvp in ItemInfoByKey)
{ {
ulong itemKey = itemKvp.Key;
ItemInfo item = itemKvp.Value; ItemInfo item = itemKvp.Value;
uint itemId = item.Item.ItemId;
if (claimedItemIds.Contains(itemId)) if (claimedKeys.Contains(itemKey))
continue; continue;
if (UserCategoryMatcher.Matches(item, category)) if (UserCategoryMatcher.Matches(item, category))
{ {
bucket.Items.Add(item); bucket.Items.Add(item);
claimedItemIds.Add(itemId); claimedKeys.Add(itemKey);
} }
} }
@@ -217,11 +258,12 @@ public static unsafe class InventoryState
// Game category bucket // Game category bucket
if (gameCategoriesEnabled) if (gameCategoriesEnabled)
{ {
foreach (var itemKvp in ItemInfoByItemId) foreach (var itemKvp in ItemInfoByKey)
{ {
ulong itemKey = itemKvp.Key;
ItemInfo info = itemKvp.Value; ItemInfo info = itemKvp.Value;
if (userCategoriesEnabled && claimedItemIds.Contains(info.Item.ItemId)) if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
continue; continue;
uint categoryKey = info.UiCategory.RowId; uint categoryKey = info.UiCategory.RowId;
@@ -253,9 +295,9 @@ public static unsafe class InventoryState
if (!BucketsByKey.TryGetValue(0u, out CategoryBucket? miscBucket)) if (!BucketsByKey.TryGetValue(0u, out CategoryBucket? miscBucket))
{ {
CategoryInfo miscInfo; CategoryInfo miscInfo;
if (ItemInfoByItemId.Count > 0) if (ItemInfoByKey.Count > 0)
{ {
var sample = ItemInfoByItemId.Values.First(); var sample = ItemInfoByKey.Values.First();
miscInfo = GetCategoryInfoForKeyCached(0u, sample); miscInfo = GetCategoryInfoForKeyCached(0u, sample);
} }
else else
@@ -278,11 +320,12 @@ public static unsafe class InventoryState
miscBucket.Used = true; miscBucket.Used = true;
} }
foreach (var itemKvp in ItemInfoByItemId) foreach (var itemKvp in ItemInfoByKey)
{ {
ulong itemKey = itemKvp.Key;
ItemInfo info = itemKvp.Value; ItemInfo info = itemKvp.Value;
if (userCategoriesEnabled && claimedItemIds.Contains(info.Item.ItemId)) if (userCategoriesEnabled && claimedKeys.Contains(itemKey))
continue; continue;
miscBucket.Items.Add(info); miscBucket.Items.Add(info);
@@ -292,19 +335,19 @@ public static unsafe class InventoryState
miscBucket.Used = false; miscBucket.Used = false;
} }
if (ItemInfoByItemId.Count != AggByItemId.Count) if (ItemInfoByKey.Count != AggByKey.Count)
{ {
RemoveKeysScratch.Clear(); RemoveKeysScratch.Clear();
foreach (var kvp in ItemInfoByItemId) foreach (var kvp in ItemInfoByKey)
{ {
uint itemId = kvp.Key; ulong key = kvp.Key;
if (!AggByItemId.ContainsKey(itemId)) if (!AggByKey.ContainsKey(key))
RemoveKeysScratch.Add(itemId); RemoveKeysScratch.Add(key);
} }
for (int i = 0; i < RemoveKeysScratch.Count; i++) for (int i = 0; i < RemoveKeysScratch.Count; i++)
ItemInfoByItemId.Remove(RemoveKeysScratch[i]); ItemInfoByKey.Remove(RemoveKeysScratch[i]);
} }
SortedCategoryKeys.Clear(); SortedCategoryKeys.Clear();
@@ -336,6 +379,11 @@ public static unsafe class InventoryState
CategoryBucket bucket = BucketsByKey[key]; CategoryBucket bucket = BucketsByKey[key];
AllCategories.Add(new CategorizedInventory(bucket.Key, bucket.Category, bucket.Items)); 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)
@@ -431,7 +479,6 @@ public static unsafe class InventoryState
return new CurrencyItem(itemId, isLimited); return new CurrencyItem(itemId, isLimited);
} }
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds) public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
{ {
if (currencyIds.Length == 0) return Array.Empty<CurrencyInfo>(); if (currencyIds.Length == 0) return Array.Empty<CurrencyInfo>();
@@ -455,7 +502,7 @@ public static unsafe class InventoryState
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
var item = Services.DataManager.GetExcelSheet<Item>().GetRow(currencyItem.ItemId); var item = Services.DataManager.GetExcelSheet<Item>().GetRow(currencyItem.ItemId);
uint amount = (uint) inventoryManager->GetInventoryItemCount(currencyItem.ItemId); uint amount = (uint)inventoryManager->GetInventoryItemCount(currencyItem.ItemId);
uint maxAmount = item.StackSize; uint maxAmount = item.StackSize;
bool isCapped = false; bool isCapped = false;
if (currencyItem.IsLimited) if (currencyItem.IsLimited)
@@ -478,8 +525,8 @@ public static unsafe class InventoryState
private static void ClearAll() private static void ClearAll()
{ {
AggByItemId.Clear(); AggByKey.Clear();
ItemInfoByItemId.Clear(); ItemInfoByKey.Clear();
foreach (var kvp in BucketsByKey) foreach (var kvp in BucketsByKey)
{ {
@@ -540,7 +587,7 @@ public static unsafe class InventoryState
public int Compare(ItemInfo? x, ItemInfo? y) public int Compare(ItemInfo? x, ItemInfo? y)
{ {
if (ReferenceEquals(x, y)) return 0; if (ReferenceEquals(x, y)) return 0;
if (x is null) return 1; // nulls last if (x is null) return 1;
if (y is null) return -1; if (y is null) return -1;
int a = x.ItemCount; int a = x.ItemCount;
@@ -562,4 +609,4 @@ public static unsafe class InventoryState
} }
private record CurrencyItem(uint ItemId, bool IsLimited); private record CurrencyItem(uint ItemId, bool IsLimited);
} }
+5 -2
View File
@@ -10,6 +10,8 @@ namespace AetherBags.Inventory;
public sealed class ItemInfo : IEquatable<ItemInfo> public sealed class ItemInfo : IEquatable<ItemInfo>
{ {
public required ulong Key { get; set; }
public required InventoryItem Item { get; set; } public required InventoryItem Item { get; set; }
public required int ItemCount { get; set; } public required int ItemCount { get; set; }
@@ -44,6 +46,7 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
public int ItemLevel => (int)Row.LevelItem.RowId; public int ItemLevel => (int)Row.LevelItem.RowId;
public int Rarity => Row.Rarity; public int Rarity => Row.Rarity;
public uint VendorPrice => Row.PriceLow; public uint VendorPrice => Row.PriceLow;
public uint StackSize => Row.StackSize;
public RowRef<ItemUICategory> UiCategory => Row.ItemUICategory; public RowRef<ItemUICategory> UiCategory => Row.ItemUICategory;
@@ -87,11 +90,11 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
=> Description.Contains(value, StringComparison.OrdinalIgnoreCase); => Description.Contains(value, StringComparison.OrdinalIgnoreCase);
public bool Equals(ItemInfo? other) public bool Equals(ItemInfo? other)
=> other is not null && Item.ItemId == other.Item.ItemId && ItemCount == other.ItemCount; => other is not null && Key == other.Key;
public override bool Equals(object? obj) public override bool Equals(object? obj)
=> obj is ItemInfo other && Equals(other); => obj is ItemInfo other && Equals(other);
public override int GetHashCode() public override int GetHashCode()
=> HashCode.Combine(Item.ItemId, ItemCount); => Key.GetHashCode();
} }
@@ -0,0 +1,7 @@
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration;
public class CategoryScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{
}
@@ -0,0 +1,8 @@
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration;
public class CurrencyScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{
}
@@ -0,0 +1,47 @@
using System;
using System.Linq;
using System.Numerics;
using AetherBags.Configuration;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration;
public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{
private readonly CheckboxNode _debugCheckboxNode = null!;
private readonly LabeledDropdownNode _stackDropDown = null!;
public GeneralScrollingAreaNode()
{
GeneralSettings config = System.Config.General;
ContentNode.ItemSpacing = 32;
_stackDropDown = new LabeledDropdownNode
{
Size = new Vector2(300, 20),
LabelText = "Stack Mode",
Options = Enum.GetNames(typeof(InventoryStackMode)).ToList(),
SelectedOption = config.StackMode.ToString(),
OnOptionSelected = selected =>
{
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
config.StackMode = parsed;
RefreshInventory();
}
};
ContentNode.AddNode(_stackDropDown);
_debugCheckboxNode = new CheckboxNode
{
Size = new Vector2(300, 20),
IsVisible = true,
String = "Debug Mode",
IsChecked = config.DebugEnabled,
OnClick = isChecked => { config.DebugEnabled = isChecked; }
};
ContentNode.AddNode(_debugCheckboxNode);
}
private void RefreshInventory() => System.AddonInventoryWindow.ManualRefresh();
}
+70
View File
@@ -0,0 +1,70 @@
using System;
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes;
public class LabeledDropdownNode : SimpleComponentNode {
private readonly GridNode _gridNode;
private readonly TextNode _labelNode;
private readonly TextDropDownNode _dropDownNode;
public LabeledDropdownNode() {
_gridNode = new GridNode {
GridSize = new GridSize(2, 1),
};
_gridNode.AttachNode(this);
_labelNode = new TextNode {
AlignmentType = AlignmentType.Bottom,
FontType = FontType.Axis,
FontSize = 14,
LineSpacing = 14,
TextColor = ColorHelper.GetColor(8),
TextOutlineColor = ColorHelper.GetColor(7),
TextFlags = TextFlags.Edge | TextFlags.AutoAdjustNodeSize,
String = string.Empty,
};
_labelNode.AttachNode(_gridNode[0, 0]);
_dropDownNode = new TextDropDownNode {
Options = new List<string>(),
};
_dropDownNode.AttachNode(_gridNode[1, 0]);
}
protected override void OnSizeChanged() {
base.OnSizeChanged();
_gridNode.Size = Size;
_labelNode.Size = _gridNode[0, 0].Size;
_dropDownNode.Size = _gridNode[1, 0].Size;
}
public required string LabelText
{
get => _labelNode.String;
set => _labelNode.String = value;
}
public Action<string>? OnOptionSelected
{
get => _dropDownNode.OnOptionSelected;
set => _dropDownNode.OnOptionSelected = value;
}
public string? SelectedOption
{
get => _dropDownNode.SelectedOption;
set => _dropDownNode.SelectedOption = value;
}
public required List<string> Options
{
get => _dropDownNode.Options!;
set => _dropDownNode.Options = value;
}
}
+13 -1
View File
@@ -28,6 +28,16 @@ public class Plugin : IDalamudPlugin
Size = new Vector2(750, 750), Size = new Vector2(750, 750),
}; };
System.AddonConfigurationWindow = new AddonConfigurationWindow
{
InternalName = "AetherBags Config",
Title = "AetherBags Config",
Size = new Vector2(640, 512),
};
Services.PluginInterface.UiBuilder.OpenMainUi += System.AddonInventoryWindow.Toggle;
Services.PluginInterface.UiBuilder.OpenConfigUi += System.AddonConfigurationWindow.Toggle;
Services.CommandManager.AddHandler("/aetherbags", new CommandInfo(OnCommand) Services.CommandManager.AddHandler("/aetherbags", new CommandInfo(OnCommand)
{ {
DisplayOrder = 1, DisplayOrder = 1,
@@ -59,6 +69,7 @@ public class Plugin : IDalamudPlugin
Services.CommandManager.RemoveHandler("/ab"); Services.CommandManager.RemoveHandler("/ab");
System.AddonInventoryWindow.Dispose(); System.AddonInventoryWindow.Dispose();
System.AddonConfigurationWindow.Dispose();
KamiToolKitLibrary.Dispose(); KamiToolKitLibrary.Dispose();
} }
@@ -72,7 +83,7 @@ public class Plugin : IDalamudPlugin
if(args.Length == 0) if(args.Length == 0)
System.AddonInventoryWindow.Toggle(); System.AddonInventoryWindow.Toggle();
if(args == "config") if(args == "config")
System.AddonInventoryWindow.Toggle(); System.AddonConfigurationWindow.Toggle();
if (args == "import-sk") if (args == "import-sk")
{ {
// Manually import from SortaKinda for testing until we have a proper config window // Manually import from SortaKinda for testing until we have a proper config window
@@ -89,6 +100,7 @@ public class Plugin : IDalamudPlugin
#if DEBUG #if DEBUG
System.AddonInventoryWindow.Toggle(); System.AddonInventoryWindow.Toggle();
System.AddonConfigurationWindow.Toggle();
#endif #endif
} }
+1
View File
@@ -6,5 +6,6 @@ namespace AetherBags;
public static class System public static class System
{ {
public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!; public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
public static SystemConfiguration Config { get; set; } = null!; public static SystemConfiguration Config { get; set; } = null!;
} }