WIP Abstraction

This commit is contained in:
Zeffuro
2025-12-29 23:40:05 +01:00
parent cc042f0349
commit 0d232e1fa0
29 changed files with 529 additions and 256 deletions
@@ -2,6 +2,7 @@ using System;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.NativeWrapper;
+29 -220
View File
@@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout;
@@ -11,43 +9,20 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Addons;
public class AddonInventoryWindow : NativeAddon
public unsafe class AddonInventoryWindow : InventoryAddonBase
{
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
private readonly InventoryCategoryPinCoordinator _pinCoordinator = new();
private readonly HashSet<InventoryCategoryNode> _hoverSubscribed = new();
private readonly MainBagState _inventoryState = new();
private InventoryNotificationNode _notificationNode = null!;
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
private TextInputWithHintNode _searchInputNode = null!;
private CircleButtonNode _settingsButtonNode = null!;
private InventoryFooterNode _footerNode = null!;
// Window constraints
private const float MinWindowWidth = 300;
private const float MaxWindowWidth = 800;
private const float MinWindowHeight = 200;
private const float MaxWindowHeight = 1000;
protected override InventoryStateBase InventoryState => _inventoryState;
// Layout settings
private const float CategorySpacing = 12;
private const float ItemSize = 40;
private const float ItemPadding = 4;
private const float FooterHeight = 28f;
private const float FooterTopSpacing = 4f;
private bool _refreshQueued;
private bool _refreshAutosizeQueued;
protected override unsafe void OnSetup(AtkUnitBase* addon)
protected override void OnSetup(AtkUnitBase* addon)
{
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{
Position = ContentStartPosition,
Size = ContentSize,
@@ -56,7 +31,7 @@ public class AddonInventoryWindow : NativeAddon
TopPadding = 4.0f,
BottomPadding = 4.0f,
};
_categoriesNode.AttachNode(this);
CategoriesNode.AttachNode(this);
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
@@ -77,49 +52,48 @@ public class AddonInventoryWindow : NativeAddon
};
_notificationNode.AttachNode(this);
_searchInputNode = new TextInputWithHintNode
SearchInputNode = new TextInputWithHintNode
{
Position = new Vector2(x, y),
Size = size,
OnInputReceived = _ => RefreshCategoriesCore(autosize: false),
};
_searchInputNode.AttachNode(this);
SearchInputNode.AttachNode(this);
_settingsButtonNode = new CircleButtonNode
SettingsButtonNode = new CircleButtonNode
{
Position = new Vector2(headerW - 48f, y),
Size = new Vector2(28f),
Icon = ButtonIcon.GearCog,
OnClick = System.AddonConfigurationWindow.Toggle
};
_settingsButtonNode.AttachNode(this);
SettingsButtonNode.AttachNode(this);
_footerNode = new InventoryFooterNode
FooterNode = new InventoryFooterNode
{
Size = ContentSize with { Y = FooterHeight },
SlotAmountText = InventoryState.GetEmptyItemSlotsString(),
SlotAmountText = _inventoryState.GetEmptySlotsString(),
};
_footerNode.AttachNode(this);
FooterNode.AttachNode(this);
LayoutContent();
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
InventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
_inventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
base.OnSetup(addon);
}
protected override unsafe void OnUpdate(AtkUnitBase* addon)
protected override void OnUpdate(AtkUnitBase* addon)
{
if (_refreshQueued)
if (RefreshQueued)
{
bool doAutosize = _refreshAutosizeQueued;
_refreshQueued = false;
_refreshAutosizeQueued = false;
bool doAutosize = RefreshAutosizeQueued;
RefreshQueued = false;
RefreshAutosizeQueued = false;
RefreshCategoriesCore(doAutosize);
}
@@ -127,191 +101,31 @@ public class AddonInventoryWindow : NativeAddon
base.OnUpdate(addon);
}
public void ManualInventoryRefresh()
{
if (!Services.ClientState.IsLoggedIn) return;
InventoryState.RefreshFromGame();
RefreshCategoriesCore(true);
}
/*public void UpdateLootedCategory(IReadOnlyList<LootedItemInfo> lootedItemInfos)
{
if (!Services.ClientState.IsLoggedIn) return;
_recentlyLootedCategoryNode?.CategorizedInventory.Items.AddRange(
lootedItemInfos.Select(x => new ItemInfo
{
ItemCount = x.Quantity,
Key = uint.MaxValue - 1,
Item = x.Item,
})
.ToList());
RefreshCategoriesCore(true);
}*/
public void ManualCurrencyRefresh()
{
if (!Services.ClientState.IsLoggedIn) return;
_footerNode.RefreshCurrencies();
FooterNode.RefreshCurrencies();
}
private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
{
InventoryState.RefreshFromGame();
_inventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
}
protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
{
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
InventoryState.RefreshFromGame();
_inventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
}
private void RefreshCategoriesCore(bool autosize)
{
_footerNode.SlotAmountText = InventoryState.GetEmptyItemSlotsString();
_footerNode.RefreshCurrencies();
string filter = _searchInputNode.SearchString.ExtractText();
IReadOnlyList<CategorizedInventory> categories = InventoryState.GetInventoryItemCategories(filter);
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
_categoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
dataList: categories,
getKeyFromData: c => c.Key,
getKeyFromNode: n => n.CategorizedInventory.Key,
updateNode: (node, data) =>
{
node.CategorizedInventory = data;
node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
},
createNodeMethod: _ => new InventoryCategoryNode
{
Size = ContentSize with { Y = 120 },
});
bool pinsChanged = _pinCoordinator.ApplyPinnedStates(_categoriesNode);
if (pinsChanged)
_hoverCoordinator.ResetAll(_categoriesNode);
WireHoverHandlers();
if (autosize) AutoSizeWindow();
else
{
LayoutContent();
_categoriesNode.RecalculateLayout();
}
}
private void WireHoverHandlers()
{
var nodes = _categoriesNode.Nodes;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode node)
continue;
if (!_hoverSubscribed.Add(node))
continue;
node.HeaderHoverChanged += (src, hovering) =>
{
_hoverCoordinator.OnCategoryHoverChanged(_categoriesNode, src, hovering);
};
}
}
private int CalculateOptimalItemsPerLine(float availableWidth)
{
return Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
}
private void LayoutContent()
{
Vector2 contentPos = ContentStartPosition;
Vector2 contentSize = ContentSize;
float footerH = FooterHeight;
_footerNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH);
_footerNode.Size = new Vector2(contentSize.X, footerH);
float gridH = contentSize.Y - footerH - FooterTopSpacing;
if (gridH < 0) gridH = 0;
_categoriesNode.Position = contentPos;
_categoriesNode.Size = new Vector2(contentSize.X, gridH);
}
private void AutoSizeWindow()
{
var nodes = _categoriesNode.Nodes;
float maxChildWidth = 0f;
int childCount = 0;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode cat)
continue;
childCount++;
float w = cat.Width;
if (w > maxChildWidth) maxChildWidth = w;
}
if (childCount == 0)
{
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
return;
}
float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
float gridBudget = Math.Max(0f, MaxWindowHeight - FooterHeight - FooterTopSpacing);
_categoriesNode.Position = ContentStartPosition;
_categoriesNode.Size = new Vector2(contentWidth, gridBudget);
_categoriesNode.RecalculateLayout();
float requiredGridHeight = _categoriesNode.GetRequiredHeight();
float requiredContentHeight = requiredGridHeight + FooterTopSpacing + FooterHeight;
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
}
private void ResizeWindow(float width, float height, bool recalcLayout)
{
SetWindowSize(width, height);
LayoutContent();
if (recalcLayout)
_categoriesNode.RecalculateLayout();
}
private void ResizeWindow(float width, float height)
=> ResizeWindow(width, height, recalcLayout: true);
public void SetNotification(InventoryNotificationInfo info)
{
Services.Framework.RunOnTick(() =>
{
if(IsOpen) _notificationNode.NotificationInfo = info;
if (IsOpen) _notificationNode.NotificationInfo = info;
}, delayTicks: 1);
}
@@ -319,13 +133,12 @@ public class AddonInventoryWindow : NativeAddon
{
Services.Framework.RunOnTick(() =>
{
if(IsOpen) _searchInputNode.SearchString = searchText;
if (IsOpen) SearchInputNode.SearchString = searchText;
RefreshCategoriesCore(autosize: true);
}, delayTicks: 1);
}
protected override unsafe void OnFinalize(AtkUnitBase* addon)
protected override void OnFinalize(AtkUnitBase* addon)
{
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
if (blockingAddonId != 0)
@@ -336,10 +149,6 @@ public class AddonInventoryWindow : NativeAddon
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_hoverSubscribed.Clear();
_refreshQueued = false;
_refreshAutosizeQueued = false;
base.OnFinalize(addon);
}
}
+1
View File
@@ -1,5 +1,6 @@
using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using KamiToolKit.Premade;
namespace AetherBags.Addons;
+208
View File
@@ -0,0 +1,208 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using AetherBags;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Nodes;
public abstract unsafe class InventoryAddonBase : NativeAddon
{
protected readonly InventoryCategoryHoverCoordinator HoverCoordinator = new();
protected readonly InventoryCategoryPinCoordinator PinCoordinator = new();
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!;
protected TextInputWithHintNode SearchInputNode = null!;
protected InventoryFooterNode FooterNode = null!;
protected CircleButtonNode SettingsButtonNode = null!;
protected virtual float MinWindowWidth => 600;
protected virtual float MaxWindowWidth => 800;
protected virtual float MinWindowHeight => 200;
protected virtual float MaxWindowHeight => 1000;
protected const float CategorySpacing = 12;
protected const float ItemSize = 40;
protected const float ItemPadding = 4;
protected const float FooterHeight = 28f;
protected const float FooterTopSpacing = 4f;
protected bool RefreshQueued;
protected bool RefreshAutosizeQueued;
protected abstract InventoryStateBase InventoryState { get; }
protected virtual bool HasFooter => true;
protected virtual bool HasPinning => true;
public void ManualInventoryRefresh()
{
if (!Services.ClientState.IsLoggedIn) return;
InventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
}
protected virtual void RefreshCategoriesCore(bool autosize)
{
if (HasFooter)
{
FooterNode.SlotAmountText = InventoryState.GetEmptySlotsString();
FooterNode.RefreshCurrencies();
}
string filter = SearchInputNode.SearchString.ExtractText();
var categories = InventoryState.GetCategories(filter);
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
dataList: categories,
getKeyFromData: categorizedInventory => categorizedInventory.Key,
getKeyFromNode: node => node.CategorizedInventory.Key,
updateNode: (node, data) =>
{
node.CategorizedInventory = data;
node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
},
createNodeMethod: _ => CreateCategoryNode());
if (HasPinning)
{
bool pinsChanged = PinCoordinator.ApplyPinnedStates(CategoriesNode);
if (pinsChanged)
HoverCoordinator.ResetAll(CategoriesNode);
}
WireHoverHandlers();
if (autosize)
AutoSizeWindow();
else
{
LayoutContent();
CategoriesNode.RecalculateLayout();
}
}
protected virtual InventoryCategoryNode CreateCategoryNode()
{
return new InventoryCategoryNode
{
Size = ContentSize with { Y = 120 },
};
}
protected void WireHoverHandlers()
{
var nodes = CategoriesNode.Nodes;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode node)
continue;
if (!HoverSubscribed.Add(node))
continue;
node.HeaderHoverChanged += (src, hovering) =>
{
HoverCoordinator.OnCategoryHoverChanged(CategoriesNode, src, hovering);
};
}
}
protected int CalculateOptimalItemsPerLine(float availableWidth)
=> Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
protected virtual void LayoutContent()
{
Vector2 contentPos = ContentStartPosition;
Vector2 contentSize = ContentSize;
if (HasFooter)
{
FooterNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - FooterHeight);
FooterNode.Size = new Vector2(contentSize.X, FooterHeight);
}
float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0);
if (gridH < 0) gridH = 0;
CategoriesNode.Position = contentPos;
CategoriesNode.Size = new Vector2(contentSize.X, gridH);
}
protected virtual void AutoSizeWindow()
{
var nodes = CategoriesNode.Nodes;
float maxChildWidth = 0f;
int childCount = 0;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode cat)
continue;
childCount++;
float w = cat.Width;
if (w > maxChildWidth) maxChildWidth = w;
}
if (childCount == 0)
{
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
return;
}
float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
float footerSpace = HasFooter ? FooterHeight + FooterTopSpacing : 0;
float gridBudget = Math.Max(0f, MaxWindowHeight - footerSpace);
CategoriesNode.Position = ContentStartPosition;
CategoriesNode.Size = new Vector2(contentWidth, gridBudget);
CategoriesNode.RecalculateLayout();
float requiredGridHeight = CategoriesNode.GetRequiredHeight();
float requiredContentHeight = requiredGridHeight + footerSpace;
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
}
protected void ResizeWindow(float width, float height, bool recalcLayout)
{
SetWindowSize(width, height);
LayoutContent();
if (recalcLayout)
CategoriesNode.RecalculateLayout();
}
protected void ResizeWindow(float width, float height)
=> ResizeWindow(width, height, recalcLayout: true);
protected override void OnFinalize(AtkUnitBase* addon)
{
HoverSubscribed.Clear();
RefreshQueued = false;
RefreshAutosizeQueued = false;
base.OnFinalize(addon);
}
}
+1
View File
@@ -1,6 +1,7 @@
using System;
using AetherBags.Helpers;
using AetherBags.Inventory;
using AetherBags.Inventory.State;
using Dalamud.Game.Command;
namespace AetherBags.Commands;
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List<ItemInfo> Items);
@@ -1,9 +1,10 @@
using AetherBags.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public static class CategoryBucketManager
{
@@ -1,7 +1,7 @@
using System.Numerics;
using KamiToolKit.Classes;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public class CategoryInfo
{
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public static class InventoryFilter
{
@@ -1,8 +1,9 @@
using AetherBags.Configuration;
using System;
using System.Text.RegularExpressions;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
internal static class UserCategoryMatcher
{
@@ -4,7 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Context;
public static unsafe class InventoryContextState
{
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Context;
public class InventoryNotificationState
{
@@ -1,4 +1,4 @@
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public readonly struct InventoryStats
{
@@ -1,11 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using System;
using System.Numerics;
using System.Text.RegularExpressions;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public sealed class ItemInfo : IEquatable<ItemInfo>
{
@@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public record LootedItemInfo(int Index, InventoryItem Item, int Quantity);
@@ -0,0 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.Scanning;
public struct AggregatedItem
{
public InventoryItem First;
public int Total;
}
@@ -1,8 +1,9 @@
using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Client.Game;
using System.Collections.Generic;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Scanning;
public static unsafe class InventoryScanner
{
@@ -46,20 +47,30 @@ public static unsafe class InventoryScanner
public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
=> ((ulong)(uint)container << 32) | (uint)slot;
// Backwards compatible
public static void ScanBags(
InventoryManager* inventoryManager,
InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> aggByKey)
=> ScanInventories(inventoryManager, stackMode, aggByKey, InventorySourceType.MainBags);
public static void ScanInventories(
InventoryManager* inventoryManager,
InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> aggByKey,
InventorySourceType source)
{
aggByKey.Clear();
var inventories = InventorySourceDefinitions.GetInventories(source);
int scannedSlots = 0;
int nonEmptySlots = 0;
int collisions = 0;
for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
for (int inventoryIndex = 0; inventoryIndex < inventories.Length; inventoryIndex++)
{
var inventoryType = BagInventories[inventoryIndex];
var inventoryType = inventories[inventoryIndex];
var container = inventoryManager->GetInventoryContainer(inventoryType);
if (container == null)
{
@@ -164,16 +175,38 @@ public static unsafe class InventoryScanner
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType);
// Backwards compability TODO: Remove
public static string GetEmptyItemSlotsString()
=> GetEmptySlotsString(InventorySourceType. MainBags);
public static string GetEmptySlotsString(InventorySourceType source)
{
uint empty = InventoryManager.Instance()->GetEmptySlotsInBag();
uint used = 140 - empty;
return $"{used}/140";
int total = InventorySourceDefinitions.GetTotalSlots(source);
uint empty = source switch
{
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer),
_ => 0,
};
uint used = (uint)total - empty;
return $"{used}/{total}";
}
private static uint GetEmptySlotsInContainer(InventoryType[] inventories)
{
uint empty = 0;
var inventoryManager = InventoryManager.Instance();
foreach (var inv in inventories)
{
var container = inventoryManager->GetInventoryContainer(inv);
if (container == null) continue;
for (int i = 0; i < container->Size; i++)
{
if (container->Items[i]. ItemId == 0)
empty++;
}
}
return empty;
}
}
public struct AggregatedItem
{
public InventoryItem First;
public int Total;
}
@@ -0,0 +1,56 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.Scanning;
public enum InventorySourceType
{
MainBags,
SaddleBag,
Retainer,
}
public static class InventorySourceDefinitions
{
public static readonly InventoryType[] MainBags =
[
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4,
];
public static readonly InventoryType[] SaddleBag =
[
InventoryType.SaddleBag1,
InventoryType.SaddleBag2,
InventoryType.PremiumSaddleBag1,
InventoryType.PremiumSaddleBag2,
];
public static readonly InventoryType[] Retainer =
[
InventoryType.RetainerPage1,
InventoryType.RetainerPage2,
InventoryType.RetainerPage3,
InventoryType.RetainerPage4,
InventoryType.RetainerPage5,
InventoryType.RetainerPage6,
InventoryType.RetainerPage7,
];
public static InventoryType[] GetInventories(InventorySourceType source) => source switch
{
InventorySourceType.MainBags => MainBags,
InventorySourceType.SaddleBag => SaddleBag,
InventorySourceType.Retainer => Retainer,
_ => MainBags,
};
public static int GetTotalSlots(InventorySourceType source) => source switch
{
InventorySourceType.MainBags => 140, // 4 * 35
InventorySourceType.SaddleBag => 70, // 2 * 35 TODO: Premium adds another 70
InventorySourceType.Retainer => 175, // 7 * 25
_ => 140,
};
}
@@ -1,12 +1,16 @@
using System.Collections.Generic;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Currency;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Context;
using AetherBags.Inventory.Items;
using AetherBags.Inventory.Scanning;
using Dalamud.Game.Inventory;
using Dalamud.Game.Inventory.InventoryEventArgTypes;
using FFXIVClientStructs.FFXIV.Client.Game;
using System.Collections.Generic;
using System.Linq;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.State;
public static unsafe class InventoryState
{
@@ -0,0 +1,109 @@
using System.Collections.Generic;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items;
using AetherBags.Inventory.Scanning;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.State;
public abstract class InventoryStateBase
{
protected readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
protected readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
protected readonly Dictionary<uint, CategoryBucket> BucketsByKey = new(capacity: 256);
protected readonly List<uint> SortedCategoryKeys = new(capacity: 256);
protected readonly List<CategorizedInventory> AllCategories = new(capacity: 256);
protected readonly List<CategorizedInventory> FilteredCategories = new(capacity: 256);
protected readonly List<UserCategoryDefinition> UserCategoriesSortedScratch = new(capacity: 64);
protected readonly List<ulong> RemoveKeysScratch = new(capacity: 256);
protected readonly HashSet<ulong> ClaimedKeys = new(capacity: 512);
public abstract InventorySourceType SourceType { get; }
public abstract InventoryType[] Inventories { get; }
public virtual unsafe void RefreshFromGame()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
{
ClearAll();
return;
}
var config = AetherBags.System.Config;
InventoryStackMode stackMode = config.General.StackMode;
AggByKey.Clear();
ItemInfoByKey.Clear();
SortedCategoryKeys.Clear();
AllCategories.Clear();
FilteredCategories.Clear();
ClaimedKeys.Clear();
InventoryScanner.ScanInventories(inventoryManager, stackMode, AggByKey, SourceType);
CategoryBucketManager.ResetBuckets(BucketsByKey);
InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey);
OnPostScan();
ApplyCategories(config);
InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch);
CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys);
CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories);
}
protected virtual void OnPostScan()
{
}
protected virtual void ApplyCategories(SystemConfiguration config)
{
bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled;
bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled;
var userCategories = config.Categories.UserCategories.Where(c => c.Enabled).ToList();
if (userCategoriesEnabled && userCategories.Count > 0)
{
CategoryBucketManager.BucketByUserCategories(
ItemInfoByKey, userCategories, BucketsByKey, ClaimedKeys, UserCategoriesSortedScratch);
}
if (gameCategoriesEnabled)
{
CategoryBucketManager.BucketByGameCategories(
ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled);
}
else
{
CategoryBucketManager.BucketUnclaimedToMisc(
ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled);
}
}
public IReadOnlyList<CategorizedInventory> GetCategories(string filter = "", bool invert = false)
=> InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert);
public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType);
protected virtual void ClearAll()
{
AggByKey.Clear();
ItemInfoByKey.Clear();
foreach (var kvp in BucketsByKey)
{
kvp.Value.Items.Clear();
kvp.Value.FilteredItems.Clear();
kvp.Value.Used = false;
}
SortedCategoryKeys.Clear();
AllCategories.Clear();
FilteredCategories.Clear();
RemoveKeysScratch.Clear();
ClaimedKeys.Clear();
}
}
@@ -0,0 +1,17 @@
using AetherBags.Inventory.Context;
using AetherBags.Inventory.Scanning;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.State;
public class MainBagState : InventoryStateBase
{
public override InventorySourceType SourceType => InventorySourceType.MainBags;
public override InventoryType[] Inventories => InventorySourceDefinitions.MainBags;
protected override void OnPostScan()
{
InventoryContextState.RefreshMaps();
InventoryContextState.RefreshBlockedSlots();
}
}
@@ -0,0 +1,10 @@
using AetherBags.Inventory.Scanning;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.State;
public class SaddleBagState : InventoryStateBase
{
public override InventorySourceType SourceType => InventorySourceType.SaddleBag;
public override InventoryType[] Inventories => InventorySourceDefinitions.SaddleBag;
}
@@ -2,6 +2,7 @@ using System;
using System.Numerics;
using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Nodes.Color;
using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -2,6 +2,8 @@ using System;
using System.Numerics;
using AetherBags.Helpers;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -33,6 +35,7 @@ public class InventoryCategoryNode : SimpleComponentNode
private string _fullHeaderText = string.Empty;
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
public Action? OnRefreshRequested { get; set; }
public InventoryCategoryNode()
{
@@ -287,7 +290,7 @@ public class InventoryCategoryNode : SimpleComponentNode
Services.Logger.Debug($"[OnPayload] Source and target are in the same container group; no move performed");
node.Payload = payload;
node.IconId = item.IconId;
System.AddonInventoryWindow.ManualInventoryRefresh();
OnRefreshRequested?.Invoke();
return;
};
@@ -297,6 +300,6 @@ public class InventoryCategoryNode : SimpleComponentNode
sourceLocation.Container, sourceLocation.Slot,
targetLocation.Container, targetLocation.Slot
);
System.AddonInventoryWindow.ManualInventoryRefresh();
OnRefreshRequested?.Invoke();
}
}
@@ -1,5 +1,6 @@
using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Inventory.Items;
using Dalamud.Game.ClientState.Keys;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Numerics;
using AetherBags.Currency;
using AetherBags.Inventory;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
@@ -1,5 +1,6 @@
using System.Numerics;
using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
+1
View File
@@ -5,6 +5,7 @@ using AetherBags.Commands;
using AetherBags.Helpers;
using AetherBags.Hooks;
using AetherBags.Inventory;
using AetherBags.Inventory.State;
using Dalamud.Plugin;
using KamiToolKit;
+2
View File
@@ -6,6 +6,8 @@ namespace AetherBags;
public static class System
{
public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
public static AddonInventoryWindow AddonSaddleBagWindow { get; set; } = null!;
public static AddonInventoryWindow AddonRetainerWindow { get; set; } = null!;
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
public static SystemConfiguration Config { get; set; } = null!;
}