WIP Abstraction
This commit is contained in:
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Context;
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
using Dalamud.Game.NativeWrapper;
|
using Dalamud.Game.NativeWrapper;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.Inventory.State;
|
||||||
using AetherBags.Nodes.Input;
|
using AetherBags.Nodes.Input;
|
||||||
using AetherBags.Nodes.Inventory;
|
using AetherBags.Nodes.Inventory;
|
||||||
using AetherBags.Nodes.Layout;
|
using AetherBags.Nodes.Layout;
|
||||||
@@ -11,43 +9,20 @@ using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit;
|
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
namespace AetherBags.Addons;
|
namespace AetherBags.Addons;
|
||||||
|
|
||||||
public class AddonInventoryWindow : NativeAddon
|
public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||||
{
|
{
|
||||||
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
|
private readonly MainBagState _inventoryState = new();
|
||||||
private readonly InventoryCategoryPinCoordinator _pinCoordinator = new();
|
|
||||||
private readonly HashSet<InventoryCategoryNode> _hoverSubscribed = new();
|
|
||||||
|
|
||||||
private InventoryNotificationNode _notificationNode = null!;
|
private InventoryNotificationNode _notificationNode = null!;
|
||||||
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
|
|
||||||
private TextInputWithHintNode _searchInputNode = null!;
|
|
||||||
private CircleButtonNode _settingsButtonNode = null!;
|
|
||||||
private InventoryFooterNode _footerNode = null!;
|
|
||||||
|
|
||||||
// Window constraints
|
protected override InventoryStateBase InventoryState => _inventoryState;
|
||||||
private const float MinWindowWidth = 300;
|
|
||||||
private const float MaxWindowWidth = 800;
|
|
||||||
private const float MinWindowHeight = 200;
|
|
||||||
private const float MaxWindowHeight = 1000;
|
|
||||||
|
|
||||||
// Layout settings
|
protected override void OnSetup(AtkUnitBase* addon)
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
@@ -56,7 +31,7 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
TopPadding = 4.0f,
|
TopPadding = 4.0f,
|
||||||
BottomPadding = 4.0f,
|
BottomPadding = 4.0f,
|
||||||
};
|
};
|
||||||
_categoriesNode.AttachNode(this);
|
CategoriesNode.AttachNode(this);
|
||||||
|
|
||||||
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
|
var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
|
||||||
|
|
||||||
@@ -77,49 +52,48 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
};
|
};
|
||||||
_notificationNode.AttachNode(this);
|
_notificationNode.AttachNode(this);
|
||||||
|
|
||||||
_searchInputNode = new TextInputWithHintNode
|
SearchInputNode = new TextInputWithHintNode
|
||||||
{
|
{
|
||||||
Position = new Vector2(x, y),
|
Position = new Vector2(x, y),
|
||||||
Size = size,
|
Size = size,
|
||||||
OnInputReceived = _ => RefreshCategoriesCore(autosize: false),
|
OnInputReceived = _ => RefreshCategoriesCore(autosize: false),
|
||||||
};
|
};
|
||||||
_searchInputNode.AttachNode(this);
|
SearchInputNode.AttachNode(this);
|
||||||
|
|
||||||
_settingsButtonNode = new CircleButtonNode
|
SettingsButtonNode = new CircleButtonNode
|
||||||
{
|
{
|
||||||
Position = new Vector2(headerW - 48f, y),
|
Position = new Vector2(headerW - 48f, y),
|
||||||
Size = new Vector2(28f),
|
Size = new Vector2(28f),
|
||||||
Icon = ButtonIcon.GearCog,
|
Icon = ButtonIcon.GearCog,
|
||||||
OnClick = System.AddonConfigurationWindow.Toggle
|
OnClick = System.AddonConfigurationWindow.Toggle
|
||||||
};
|
};
|
||||||
_settingsButtonNode.AttachNode(this);
|
SettingsButtonNode.AttachNode(this);
|
||||||
|
|
||||||
_footerNode = new InventoryFooterNode
|
FooterNode = new InventoryFooterNode
|
||||||
{
|
{
|
||||||
Size = ContentSize with { Y = FooterHeight },
|
Size = ContentSize with { Y = FooterHeight },
|
||||||
SlotAmountText = InventoryState.GetEmptyItemSlotsString(),
|
SlotAmountText = _inventoryState.GetEmptySlotsString(),
|
||||||
};
|
};
|
||||||
_footerNode.AttachNode(this);
|
FooterNode.AttachNode(this);
|
||||||
|
|
||||||
LayoutContent();
|
LayoutContent();
|
||||||
|
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
|
||||||
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
InventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
|
RefreshCategoriesCore(autosize: true);
|
||||||
RefreshCategoriesCore(autosize: true);
|
|
||||||
|
|
||||||
base.OnSetup(addon);
|
base.OnSetup(addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override unsafe void OnUpdate(AtkUnitBase* addon)
|
protected override void OnUpdate(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
if (_refreshQueued)
|
if (RefreshQueued)
|
||||||
{
|
{
|
||||||
bool doAutosize = _refreshAutosizeQueued;
|
bool doAutosize = RefreshAutosizeQueued;
|
||||||
_refreshQueued = false;
|
RefreshQueued = false;
|
||||||
_refreshAutosizeQueued = false;
|
RefreshAutosizeQueued = false;
|
||||||
|
|
||||||
RefreshCategoriesCore(doAutosize);
|
RefreshCategoriesCore(doAutosize);
|
||||||
}
|
}
|
||||||
@@ -127,191 +101,31 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
base.OnUpdate(addon);
|
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()
|
public void ManualCurrencyRefresh()
|
||||||
{
|
{
|
||||||
if (!Services.ClientState.IsLoggedIn) return;
|
if (!Services.ClientState.IsLoggedIn) return;
|
||||||
_footerNode.RefreshCurrencies();
|
FooterNode.RefreshCurrencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
|
private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
|
||||||
{
|
{
|
||||||
InventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
|
|
||||||
RefreshCategoriesCore(autosize: true);
|
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);
|
base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
|
||||||
|
|
||||||
InventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
|
|
||||||
RefreshCategoriesCore(autosize: true);
|
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)
|
public void SetNotification(InventoryNotificationInfo info)
|
||||||
{
|
{
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
{
|
{
|
||||||
if(IsOpen) _notificationNode.NotificationInfo = info;
|
if (IsOpen) _notificationNode.NotificationInfo = info;
|
||||||
}, delayTicks: 1);
|
}, delayTicks: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,13 +133,12 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
{
|
{
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
{
|
{
|
||||||
if(IsOpen) _searchInputNode.SearchString = searchText;
|
if (IsOpen) SearchInputNode.SearchString = searchText;
|
||||||
RefreshCategoriesCore(autosize: true);
|
RefreshCategoriesCore(autosize: true);
|
||||||
}, delayTicks: 1);
|
}, delayTicks: 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
protected override unsafe void OnFinalize(AtkUnitBase* addon)
|
|
||||||
{
|
{
|
||||||
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
||||||
if (blockingAddonId != 0)
|
if (blockingAddonId != 0)
|
||||||
@@ -336,10 +149,6 @@ public class AddonInventoryWindow : NativeAddon
|
|||||||
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
|
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
|
||||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
_hoverSubscribed.Clear();
|
|
||||||
_refreshQueued = false;
|
|
||||||
_refreshAutosizeQueued = false;
|
|
||||||
|
|
||||||
base.OnFinalize(addon);
|
base.OnFinalize(addon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
using KamiToolKit.Premade;
|
using KamiToolKit.Premade;
|
||||||
|
|
||||||
namespace AetherBags.Addons;
|
namespace AetherBags.Addons;
|
||||||
|
|||||||
@@ -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,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.State;
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
|
|
||||||
namespace AetherBags.Commands;
|
namespace AetherBags.Commands;
|
||||||
|
|||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
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);
|
public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List<ItemInfo> Items);
|
||||||
+3
-2
@@ -1,9 +1,10 @@
|
|||||||
using AetherBags.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using AetherBags.Configuration;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Categories;
|
||||||
|
|
||||||
public static class CategoryBucketManager
|
public static class CategoryBucketManager
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Categories;
|
||||||
|
|
||||||
public class CategoryInfo
|
public class CategoryInfo
|
||||||
{
|
{
|
||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Categories;
|
||||||
|
|
||||||
public static class InventoryFilter
|
public static class InventoryFilter
|
||||||
{
|
{
|
||||||
+3
-2
@@ -1,8 +1,9 @@
|
|||||||
using AetherBags.Configuration;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using AetherBags.Configuration;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Categories;
|
||||||
|
|
||||||
internal static class UserCategoryMatcher
|
internal static class UserCategoryMatcher
|
||||||
{
|
{
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
|
using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Context;
|
||||||
|
|
||||||
public static unsafe class InventoryContextState
|
public static unsafe class InventoryContextState
|
||||||
{
|
{
|
||||||
+1
-1
@@ -2,7 +2,7 @@ using System.Collections.Generic;
|
|||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Lumina.Text.ReadOnly;
|
using Lumina.Text.ReadOnly;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Context;
|
||||||
|
|
||||||
public class InventoryNotificationState
|
public class InventoryNotificationState
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Items;
|
||||||
|
|
||||||
public readonly struct InventoryStats
|
public readonly struct InventoryStats
|
||||||
{
|
{
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using Lumina.Excel;
|
|
||||||
using Lumina.Excel.Sheets;
|
|
||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Text.RegularExpressions;
|
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>
|
public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||||
{
|
{
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.Items;
|
||||||
|
|
||||||
public record LootedItemInfo(int Index, InventoryItem Item, int Quantity);
|
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;
|
||||||
|
}
|
||||||
+47
-14
@@ -1,8 +1,9 @@
|
|||||||
using AetherBags.Configuration;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
using System.Collections.Generic;
|
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
|
public static unsafe class InventoryScanner
|
||||||
{
|
{
|
||||||
@@ -46,20 +47,30 @@ public static unsafe class InventoryScanner
|
|||||||
public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
|
public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
|
||||||
=> ((ulong)(uint)container << 32) | (uint)slot;
|
=> ((ulong)(uint)container << 32) | (uint)slot;
|
||||||
|
|
||||||
|
// Backwards compatible
|
||||||
public static void ScanBags(
|
public static void ScanBags(
|
||||||
InventoryManager* inventoryManager,
|
InventoryManager* inventoryManager,
|
||||||
InventoryStackMode stackMode,
|
InventoryStackMode stackMode,
|
||||||
Dictionary<ulong, AggregatedItem> aggByKey)
|
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();
|
aggByKey.Clear();
|
||||||
|
|
||||||
|
var inventories = InventorySourceDefinitions.GetInventories(source);
|
||||||
|
|
||||||
int scannedSlots = 0;
|
int scannedSlots = 0;
|
||||||
int nonEmptySlots = 0;
|
int nonEmptySlots = 0;
|
||||||
int collisions = 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);
|
var container = inventoryManager->GetInventoryContainer(inventoryType);
|
||||||
if (container == null)
|
if (container == null)
|
||||||
{
|
{
|
||||||
@@ -164,16 +175,38 @@ public static unsafe class InventoryScanner
|
|||||||
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
|
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
|
||||||
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType);
|
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType);
|
||||||
|
|
||||||
|
// Backwards compability TODO: Remove
|
||||||
public static string GetEmptyItemSlotsString()
|
public static string GetEmptyItemSlotsString()
|
||||||
|
=> GetEmptySlotsString(InventorySourceType. MainBags);
|
||||||
|
|
||||||
|
public static string GetEmptySlotsString(InventorySourceType source)
|
||||||
{
|
{
|
||||||
uint empty = InventoryManager.Instance()->GetEmptySlotsInBag();
|
int total = InventorySourceDefinitions.GetTotalSlots(source);
|
||||||
uint used = 140 - empty;
|
uint empty = source switch
|
||||||
return $"{used}/140";
|
{
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
+7
-3
@@ -1,12 +1,16 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Currency;
|
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;
|
||||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
|
|
||||||
namespace AetherBags.Inventory;
|
namespace AetherBags.Inventory.State;
|
||||||
|
|
||||||
public static unsafe class InventoryState
|
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 System.Numerics;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Nodes.Color;
|
using AetherBags.Nodes.Color;
|
||||||
using Dalamud.Utility;
|
using Dalamud.Utility;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ using System;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Nodes.Layout;
|
using AetherBags.Nodes.Layout;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
@@ -33,6 +35,7 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
private string _fullHeaderText = string.Empty;
|
private string _fullHeaderText = string.Empty;
|
||||||
|
|
||||||
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
||||||
|
public Action? OnRefreshRequested { get; set; }
|
||||||
|
|
||||||
public InventoryCategoryNode()
|
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");
|
Services.Logger.Debug($"[OnPayload] Source and target are in the same container group; no move performed");
|
||||||
node.Payload = payload;
|
node.Payload = payload;
|
||||||
node.IconId = item.IconId;
|
node.IconId = item.IconId;
|
||||||
System.AddonInventoryWindow.ManualInventoryRefresh();
|
OnRefreshRequested?.Invoke();
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -297,6 +300,6 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
sourceLocation.Container, sourceLocation.Slot,
|
sourceLocation.Container, sourceLocation.Slot,
|
||||||
targetLocation.Container, targetLocation.Slot
|
targetLocation.Container, targetLocation.Slot
|
||||||
);
|
);
|
||||||
System.AddonInventoryWindow.ManualInventoryRefresh();
|
OnRefreshRequested?.Invoke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System.Collections.Generic;
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Currency;
|
using AetherBags.Currency;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.State;
|
||||||
using AetherBags.Nodes.Currency;
|
using AetherBags.Nodes.Currency;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Context;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using KamiToolKit.Classes.Timelines;
|
using KamiToolKit.Classes.Timelines;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using AetherBags.Commands;
|
|||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Hooks;
|
using AetherBags.Hooks;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.State;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ 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 AddonInventoryWindow AddonSaddleBagWindow { get; set; } = null!;
|
||||||
|
public static AddonInventoryWindow AddonRetainerWindow { get; set; } = null!;
|
||||||
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
||||||
public static SystemConfiguration Config { get; set; } = null!;
|
public static SystemConfiguration Config { get; set; } = null!;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user