Pinning, Hoisting, Recently Lotted
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
|
||||||
using AetherBags.Inventory.Context;
|
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;
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Inventory.State;
|
using AetherBags.Inventory.State;
|
||||||
using AetherBags.Nodes.Input;
|
using AetherBags.Nodes.Input;
|
||||||
using AetherBags.Nodes.Inventory;
|
using AetherBags.Nodes.Inventory;
|
||||||
@@ -15,6 +17,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
{
|
{
|
||||||
private readonly MainBagState _inventoryState = new();
|
private readonly MainBagState _inventoryState = new();
|
||||||
private InventoryNotificationNode _notificationNode = null!;
|
private InventoryNotificationNode _notificationNode = null!;
|
||||||
|
private LootedItemsCategoryNode _lootedCategoryNode = null!;
|
||||||
|
|
||||||
protected override InventoryStateBase InventoryState => _inventoryState;
|
protected override InventoryStateBase InventoryState => _inventoryState;
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
{
|
{
|
||||||
InitializeBackgroundDropTarget();
|
InitializeBackgroundDropTarget();
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
@@ -33,6 +36,13 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
};
|
};
|
||||||
CategoriesNode.AttachNode(this);
|
CategoriesNode.AttachNode(this);
|
||||||
|
|
||||||
|
_lootedCategoryNode = new LootedItemsCategoryNode
|
||||||
|
{
|
||||||
|
ItemsPerLine = 10,
|
||||||
|
OnDismissItem = OnDismissLootedItem,
|
||||||
|
OnClearAll = OnClearAllLootedItems,
|
||||||
|
};
|
||||||
|
|
||||||
var header = CalculateHeaderLayout(addon);
|
var header = CalculateHeaderLayout(addon);
|
||||||
|
|
||||||
_notificationNode = new InventoryNotificationNode
|
_notificationNode = new InventoryNotificationNode
|
||||||
@@ -71,14 +81,69 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
_isSetupComplete = true;
|
System.LootedItemsTracker.OnLootedItemsChanged += OnLootedItemsChanged;
|
||||||
|
|
||||||
|
IsSetupComplete = true;
|
||||||
|
|
||||||
_inventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
|
|
||||||
|
var existingLoot = System.LootedItemsTracker.LootedItems;
|
||||||
|
if (existingLoot.Count > 0)
|
||||||
|
{
|
||||||
|
UpdateLootedCategory(existingLoot);
|
||||||
|
}
|
||||||
|
|
||||||
RefreshCategoriesCore(autosize: true);
|
RefreshCategoriesCore(autosize: true);
|
||||||
|
|
||||||
base.OnSetup(addon);
|
base.OnSetup(addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnLootedItemsChanged(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
|
{
|
||||||
|
if (!IsOpen || !IsSetupComplete) return;
|
||||||
|
|
||||||
|
Services.Framework.RunOnTick(() =>
|
||||||
|
{
|
||||||
|
if (!IsOpen) return;
|
||||||
|
UpdateLootedCategory(lootedItems);
|
||||||
|
}, delayTicks: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateLootedCategory(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
|
{
|
||||||
|
_lootedCategoryNode.UpdateLootedItems(lootedItems);
|
||||||
|
|
||||||
|
if (lootedItems.Count > 0)
|
||||||
|
{
|
||||||
|
if (CategoriesNode.HoistedNode != _lootedCategoryNode)
|
||||||
|
{
|
||||||
|
CategoriesNode.SetHoistedNode(_lootedCategoryNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
using (CategoriesNode.DeferRecalculateLayout())
|
||||||
|
{
|
||||||
|
if (CategoriesNode.HoistedNode == _lootedCategoryNode)
|
||||||
|
{
|
||||||
|
CategoriesNode.SetHoistedNode(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDismissLootedItem(int index)
|
||||||
|
{
|
||||||
|
System.LootedItemsTracker.RemoveByIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnClearAllLootedItems()
|
||||||
|
{
|
||||||
|
System.LootedItemsTracker.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
public void ManualCurrencyRefresh()
|
public void ManualCurrencyRefresh()
|
||||||
{
|
{
|
||||||
if (!Services.ClientState.IsLoggedIn) return;
|
if (!Services.ClientState.IsLoggedIn) return;
|
||||||
@@ -95,6 +160,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
protected override void OnFinalize(AtkUnitBase* addon)
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
|
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
|
||||||
|
|
||||||
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
||||||
if (blockingAddonId != 0)
|
if (blockingAddonId != 0)
|
||||||
{
|
{
|
||||||
@@ -103,7 +170,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
_isSetupComplete = false;
|
IsSetupComplete = false;
|
||||||
base.OnFinalize(addon);
|
base.OnFinalize(addon);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
|
|
||||||
WindowNode?.AddColor = _tintColor;
|
WindowNode?.AddColor = _tintColor;
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
@@ -107,7 +107,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
LayoutContent();
|
LayoutContent();
|
||||||
|
|
||||||
_inventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
_isSetupComplete = true;
|
IsSetupComplete = true;
|
||||||
|
|
||||||
RefreshCategoriesCore(autosize: true);
|
RefreshCategoriesCore(autosize: true);
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
|
|
||||||
protected override void RefreshCategoriesCore(bool autosize)
|
protected override void RefreshCategoriesCore(bool autosize)
|
||||||
{
|
{
|
||||||
if (!_isSetupComplete)
|
if (!IsSetupComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
||||||
@@ -179,7 +179,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
|
|
||||||
protected override void OnFinalize(AtkUnitBase* addon)
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
_isSetupComplete = false;
|
IsSetupComplete = false;
|
||||||
|
|
||||||
CloseRetainerWindows();
|
CloseRetainerWindows();
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
WindowNode?.AddColor = _tintColor;
|
WindowNode?.AddColor = _tintColor;
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
@@ -80,7 +80,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
_inventoryState.RefreshFromGame();
|
_inventoryState.RefreshFromGame();
|
||||||
|
|
||||||
_isSetupComplete = true;
|
IsSetupComplete = true;
|
||||||
|
|
||||||
RefreshCategoriesCore(autosize: true);
|
RefreshCategoriesCore(autosize: true);
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
protected override void RefreshCategoriesCore(bool autosize)
|
protected override void RefreshCategoriesCore(bool autosize)
|
||||||
{
|
{
|
||||||
if (!_isSetupComplete)
|
if (!IsSetupComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
||||||
@@ -99,7 +99,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
protected override void OnFinalize(AtkUnitBase* addon)
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
_isSetupComplete = false;
|
IsSetupComplete = false;
|
||||||
|
|
||||||
if (System.Config.General.HideGameSaddleBags)
|
if (System.Config.General.HideGameSaddleBags)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
|
||||||
using AetherBags.Inventory.Categories;
|
using AetherBags.Inventory.Categories;
|
||||||
using KamiToolKit.Premade;
|
using KamiToolKit.Premade;
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
|
||||||
namespace AetherBags.Addons;
|
namespace AetherBags.Addons;
|
||||||
|
|
||||||
public interface IInventoryWindow
|
public interface IInventoryWindow
|
||||||
@@ -8,4 +10,5 @@ public interface IInventoryWindow
|
|||||||
void ManualRefresh();
|
void ManualRefresh();
|
||||||
void ItemRefresh();
|
void ItemRefresh();
|
||||||
void SetSearchText(string searchText);
|
void SetSearchText(string searchText);
|
||||||
|
InventoryStats GetStats();
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ using AetherBags.Helpers;
|
|||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Categories;
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Inventory.Scanning;
|
using AetherBags.Inventory.Scanning;
|
||||||
using AetherBags.Inventory.State;
|
using AetherBags.Inventory.State;
|
||||||
using AetherBags.Nodes.Input;
|
using AetherBags.Nodes.Input;
|
||||||
@@ -27,7 +28,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
||||||
|
|
||||||
protected DragDropNode BackgroundDropTarget = null!;
|
protected DragDropNode BackgroundDropTarget = null!;
|
||||||
protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!;
|
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
||||||
protected TextInputWithButtonNode SearchInputNode = null!;
|
protected TextInputWithButtonNode SearchInputNode = null!;
|
||||||
protected InventoryFooterNode FooterNode = null!;
|
protected InventoryFooterNode FooterNode = null!;
|
||||||
protected TextNode? SlotCounterNode { get; set; }
|
protected TextNode? SlotCounterNode { get; set; }
|
||||||
@@ -49,8 +50,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
protected bool RefreshQueued;
|
protected bool RefreshQueued;
|
||||||
protected bool RefreshAutosizeQueued;
|
protected bool RefreshAutosizeQueued;
|
||||||
private bool _isRefreshing;
|
protected bool IsSetupComplete;
|
||||||
protected bool _isSetupComplete;
|
|
||||||
|
|
||||||
protected abstract InventoryStateBase InventoryState { get; }
|
protected abstract InventoryStateBase InventoryState { get; }
|
||||||
|
|
||||||
@@ -59,13 +59,14 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
protected virtual bool HasSlotCounter => false;
|
protected virtual bool HasSlotCounter => false;
|
||||||
|
|
||||||
private readonly HashSet<uint> _searchMatchScratch = new();
|
private readonly HashSet<uint> _searchMatchScratch = new();
|
||||||
|
private bool _isRefreshing;
|
||||||
|
|
||||||
public void ManualRefresh()
|
public void ManualRefresh()
|
||||||
{
|
{
|
||||||
if (!IsOpen) return;
|
if (!IsOpen) return;
|
||||||
if (!Services.ClientState.IsLoggedIn) return;
|
if (!Services.ClientState.IsLoggedIn) return;
|
||||||
if (_isRefreshing) return;
|
if (_isRefreshing) return;
|
||||||
if (!_isSetupComplete) return;
|
if (!IsSetupComplete) return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -82,6 +83,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty;
|
public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty;
|
||||||
|
|
||||||
|
public InventoryStats GetStats() => InventoryState.GetStats();
|
||||||
|
|
||||||
public virtual void SetSearchText(string searchText)
|
public virtual void SetSearchText(string searchText)
|
||||||
{
|
{
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
@@ -93,7 +96,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
public void RefreshFromLifecycle()
|
public void RefreshFromLifecycle()
|
||||||
{
|
{
|
||||||
if (!_isSetupComplete) return;
|
if (!IsSetupComplete) return;
|
||||||
if (!IsOpen) return;
|
if (!IsOpen) return;
|
||||||
if (_isRefreshing) return;
|
if (_isRefreshing) return;
|
||||||
|
|
||||||
@@ -111,7 +114,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
protected virtual void RefreshCategoriesCore(bool autosize)
|
protected virtual void RefreshCategoriesCore(bool autosize)
|
||||||
{
|
{
|
||||||
if (!_isSetupComplete)
|
if (!IsSetupComplete)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var config = System.Config.General;
|
var config = System.Config.General;
|
||||||
@@ -166,7 +169,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
||||||
dataList: categories,
|
dataList: categories,
|
||||||
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
||||||
getKeyFromNode: node => node.CategorizedInventory.Key,
|
getKeyFromNode: node => node.Key,
|
||||||
updateNode: (node, data) =>
|
updateNode: (node, data) =>
|
||||||
{
|
{
|
||||||
node.CategorizedInventory = data;
|
node.CategorizedInventory = data;
|
||||||
@@ -394,7 +397,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
protected void ResizeWindow(float width, float height)
|
protected void ResizeWindow(float width, float height)
|
||||||
=> ResizeWindow(width, height, recalcLayout: true);
|
=> ResizeWindow(width, height, recalcLayout: true);
|
||||||
|
|
||||||
public void ItemRefresh() => RefreshCategoriesCore(false);
|
public void ItemRefresh()
|
||||||
|
{
|
||||||
|
if (!IsOpen) return;
|
||||||
|
if (!IsSetupComplete) return;
|
||||||
|
|
||||||
|
RefreshCategoriesCore(false);
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AetherBags.Addons;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.State;
|
using AetherBags.Inventory.Items;
|
||||||
using Dalamud.Game.Command;
|
using Dalamud.Game.Command;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
|
|
||||||
namespace AetherBags.Commands;
|
namespace AetherBags.Commands;
|
||||||
|
|
||||||
@@ -30,7 +31,7 @@ public class CommandHandler : IDisposable
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe void OnCommand(string command, string args)
|
private void OnCommand(string command, string args)
|
||||||
{
|
{
|
||||||
var argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
var argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
|
||||||
var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty;
|
var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty;
|
||||||
@@ -88,8 +89,7 @@ public class CommandHandler : IDisposable
|
|||||||
|
|
||||||
case "count":
|
case "count":
|
||||||
case "stats":
|
case "stats":
|
||||||
var stats = InventoryState.GetInventoryStats();
|
PrintInventoryStats();
|
||||||
PrintChat($"{stats.UsedSlots}/{stats.TotalSlots} slots used ({stats.UsagePercent:F0}%) | {stats.TotalItems} unique items | {stats.CategoryCount} categories");
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "saddle":
|
case "saddle":
|
||||||
@@ -111,6 +111,40 @@ public class CommandHandler : IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void PrintInventoryStats()
|
||||||
|
{
|
||||||
|
var openWindows = new List<(string Name, IInventoryWindow Window)>();
|
||||||
|
|
||||||
|
if (System.AddonInventoryWindow.IsOpen)
|
||||||
|
openWindows.Add(("Main", System.AddonInventoryWindow));
|
||||||
|
if (System.AddonSaddleBagWindow.IsOpen)
|
||||||
|
openWindows.Add(("Saddle", System.AddonSaddleBagWindow));
|
||||||
|
if (System.AddonRetainerWindow.IsOpen)
|
||||||
|
openWindows.Add(("Retainer", System.AddonRetainerWindow));
|
||||||
|
|
||||||
|
if (openWindows.Count == 0)
|
||||||
|
{
|
||||||
|
PrintChat("No inventory windows are open. Open an inventory to see stats.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var (name, window) in openWindows)
|
||||||
|
{
|
||||||
|
var stats = window.GetStats();
|
||||||
|
PrintChat($"[{name}] {stats.UsedSlots}/{stats.TotalSlots} slots ({stats.UsagePercent:F0}%) | {stats.TotalItems} items | {stats.CategoryCount} categories");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (openWindows.Count > 1)
|
||||||
|
{
|
||||||
|
var combined = new InventoryStats();
|
||||||
|
foreach (var (_, window) in openWindows)
|
||||||
|
{
|
||||||
|
combined += window.GetStats();
|
||||||
|
}
|
||||||
|
PrintChat($"[Total] {combined.UsedSlots}/{combined.TotalSlots} slots ({combined.UsagePercent:F0}%) | {combined.TotalItems} items | {combined.CategoryCount} categories");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void HandleSearch(string searchTerm)
|
private void HandleSearch(string searchTerm)
|
||||||
{
|
{
|
||||||
if (!System.AddonInventoryWindow.IsOpen)
|
if (!System.AddonInventoryWindow.IsOpen)
|
||||||
|
|||||||
@@ -4,8 +4,36 @@ public class SystemConfiguration
|
|||||||
{
|
{
|
||||||
public const string FileName = "AetherBags.json";
|
public const string FileName = "AetherBags.json";
|
||||||
|
|
||||||
|
private GeneralSettings _general = new();
|
||||||
|
private CategorySettings _categories = new();
|
||||||
|
private CurrencySettings _currency = new();
|
||||||
|
|
||||||
public GeneralSettings General { get; set; } = new();
|
public GeneralSettings General
|
||||||
public CategorySettings Categories { get; set; } = new();
|
{
|
||||||
public CurrencySettings Currency { get; set; } = new();
|
get => _general;
|
||||||
|
set => _general = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CategorySettings Categories
|
||||||
|
{
|
||||||
|
get => _categories;
|
||||||
|
set => _categories = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public CurrencySettings Currency
|
||||||
|
{
|
||||||
|
get => _currency;
|
||||||
|
set => _currency = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ensures all nested config objects are initialized. Call after deserialization.
|
||||||
|
/// </summary>
|
||||||
|
public void EnsureInitialized()
|
||||||
|
{
|
||||||
|
_general ??= new();
|
||||||
|
_categories ??= new();
|
||||||
|
_currency ??= new();
|
||||||
|
_categories.UserCategories ??= new();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AetherBags;
|
|
||||||
using Dalamud.Game.Addon.Lifecycle;
|
using Dalamud.Game.Addon.Lifecycle;
|
||||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||||
using Dalamud.Plugin.Services;
|
using Dalamud.Plugin.Services;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
|
||||||
|
namespace AetherBags.Extensions;
|
||||||
|
|
||||||
public static class AddonLifecycleExtensions {
|
public static class AddonLifecycleExtensions {
|
||||||
extension(IAddonLifecycle addonLifecycle) {
|
extension(IAddonLifecycle addonLifecycle) {
|
||||||
public void LogAddon(string addonName, params AddonEvent[] loggedModules) {
|
public void LogAddon(string addonName, params AddonEvent[] loggedModules) {
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
|
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using Lumina.Text.ReadOnly;
|
|
||||||
using Lumina.Text;
|
|
||||||
|
|
||||||
namespace AetherBags.Extensions;
|
namespace AetherBags.Extensions;
|
||||||
|
|
||||||
public static unsafe class DragDropPayloadExtensions
|
public static class DragDropPayloadExtensions
|
||||||
{
|
{
|
||||||
extension(DragDropPayload payload)
|
extension(DragDropPayload payload)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,10 +8,16 @@ public static class LoggerExtensions
|
|||||||
{
|
{
|
||||||
if (System.Config?.General?.DebugEnabled == true)
|
if (System.Config?.General?.DebugEnabled == true)
|
||||||
{
|
{
|
||||||
Services.Logger.DebugOnly(message);
|
Services.Logger.Debug(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void DebugOnly(string message, params object[] args) => DebugOnly(logger, string.Format(message, args));
|
public void DebugOnly(string message, params object[] args)
|
||||||
|
{
|
||||||
|
if (System.Config?.General?.DebugEnabled == true)
|
||||||
|
{
|
||||||
|
Services.Logger.Debug(message, args);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
using System;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||||
|
|||||||
@@ -87,11 +87,17 @@ public static class Util
|
|||||||
private static SystemConfiguration LoadConfig()
|
private static SystemConfiguration LoadConfig()
|
||||||
{
|
{
|
||||||
FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
|
FileInfo file = JsonFileHelper.GetFileInfo(SystemConfiguration.FileName);
|
||||||
return JsonFileHelper.LoadFile<SystemConfiguration>(file.FullName);
|
var config = JsonFileHelper.LoadFile<SystemConfiguration>(file.FullName);
|
||||||
|
config?.EnsureInitialized();
|
||||||
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static SystemConfiguration LoadConfigOrDefault()
|
public static SystemConfiguration LoadConfigOrDefault()
|
||||||
=> LoadConfig() ?? new SystemConfiguration();
|
{
|
||||||
|
var config = LoadConfig() ?? new SystemConfiguration();
|
||||||
|
config.EnsureInitialized();
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
public static SystemConfiguration ResetConfig()
|
public static SystemConfiguration ResetConfig()
|
||||||
=> new SystemConfiguration();
|
=> new SystemConfiguration();
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using Dalamud.Plugin;
|
|
||||||
using Dalamud.Plugin.Ipc;
|
|
||||||
|
|
||||||
namespace AetherBags.IPC;
|
namespace AetherBags.IPC;
|
||||||
|
|
||||||
|
|||||||
@@ -8,31 +8,46 @@ namespace AetherBags.Inventory;
|
|||||||
public static unsafe class InventoryOrchestrator
|
public static unsafe class InventoryOrchestrator
|
||||||
{
|
{
|
||||||
private static readonly InventoryNotificationState NotificationState = new();
|
private static readonly InventoryNotificationState NotificationState = new();
|
||||||
|
private static bool _isRefreshing;
|
||||||
|
|
||||||
public static void RefreshAll(bool updateMaps = true)
|
public static void RefreshAll(bool updateMaps = true)
|
||||||
{
|
{
|
||||||
|
if (_isRefreshing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isRefreshing = true;
|
||||||
|
|
||||||
if (updateMaps)
|
if (updateMaps)
|
||||||
{
|
{
|
||||||
InventoryContextState.RefreshMaps();
|
InventoryContextState.RefreshMaps();
|
||||||
InventoryContextState.RefreshBlockedSlots();
|
InventoryContextState.RefreshBlockedSlots();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!HasAnyWindowOpen())
|
||||||
|
return;
|
||||||
|
|
||||||
var agent = AgentInventory.Instance();
|
var agent = AgentInventory.Instance();
|
||||||
var contextId = agent != null ? agent->OpenTitleId : 0;
|
var contextId = agent != null ? agent->OpenTitleId : 0;
|
||||||
var notification = NotificationState.GetNotificationInfo(contextId);
|
var notification = NotificationState.GetNotificationInfo(contextId);
|
||||||
|
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
{
|
{
|
||||||
if (System.AddonInventoryWindow.IsOpen)
|
if (notification != null && System.AddonInventoryWindow.IsOpen)
|
||||||
System.AddonInventoryWindow.SetNotification(notification!);
|
System.AddonInventoryWindow.SetNotification(notification);
|
||||||
|
|
||||||
foreach (var window in GetAllWindows())
|
foreach (var window in GetAllWindows())
|
||||||
{
|
{
|
||||||
if (window.IsOpen)
|
|
||||||
window.ManualRefresh();
|
window.ManualRefresh();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isRefreshing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void CloseAll()
|
public static void CloseAll()
|
||||||
{
|
{
|
||||||
@@ -44,6 +59,9 @@ public static unsafe class InventoryOrchestrator
|
|||||||
|
|
||||||
public static void RefreshHighlights()
|
public static void RefreshHighlights()
|
||||||
{
|
{
|
||||||
|
if (!HasAnyWindowOpen())
|
||||||
|
return;
|
||||||
|
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
{
|
{
|
||||||
foreach (var window in GetAllWindows())
|
foreach (var window in GetAllWindows())
|
||||||
@@ -53,10 +71,23 @@ public static unsafe class InventoryOrchestrator
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool HasAnyWindowOpen()
|
||||||
|
{
|
||||||
|
foreach (var window in GetAllWindows())
|
||||||
|
{
|
||||||
|
if (window.IsOpen)
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private static IEnumerable<IInventoryWindow> GetAllWindows()
|
private static IEnumerable<IInventoryWindow> GetAllWindows()
|
||||||
{
|
{
|
||||||
|
if (System.AddonInventoryWindow != null)
|
||||||
yield return System.AddonInventoryWindow;
|
yield return System.AddonInventoryWindow;
|
||||||
|
if (System.AddonSaddleBagWindow != null)
|
||||||
yield return System.AddonSaddleBagWindow;
|
yield return System.AddonSaddleBagWindow;
|
||||||
|
if (System.AddonRetainerWindow != null)
|
||||||
yield return System.AddonRetainerWindow;
|
yield return System.AddonRetainerWindow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,4 +9,13 @@ public readonly struct InventoryStats
|
|||||||
public int CategoryCount { get; init; }
|
public int CategoryCount { get; init; }
|
||||||
public int UsedSlots => TotalSlots - EmptySlots;
|
public int UsedSlots => TotalSlots - EmptySlots;
|
||||||
public float UsagePercent => TotalSlots > 0 ? (float)UsedSlots / TotalSlots * 100f : 0f;
|
public float UsagePercent => TotalSlots > 0 ? (float)UsedSlots / TotalSlots * 100f : 0f;
|
||||||
|
|
||||||
|
public static InventoryStats operator +(InventoryStats a, InventoryStats b) => new()
|
||||||
|
{
|
||||||
|
TotalItems = a.TotalItems + b.TotalItems,
|
||||||
|
TotalQuantity = a.TotalQuantity + b.TotalQuantity,
|
||||||
|
EmptySlots = a.EmptySlots + b.EmptySlots,
|
||||||
|
TotalSlots = a.TotalSlots + b.TotalSlots,
|
||||||
|
CategoryCount = a.CategoryCount + b.CategoryCount,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
using AetherBags.Inventory.Scanning;
|
||||||
|
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
|
namespace AetherBags.Inventory;
|
||||||
|
|
||||||
|
public sealed unsafe class LootedItemsTracker : IDisposable
|
||||||
|
{
|
||||||
|
private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
|
||||||
|
|
||||||
|
private readonly List<LootedItemInfo> _lootedItems = new(capacity: 64);
|
||||||
|
private bool _isEnabled;
|
||||||
|
|
||||||
|
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
||||||
|
|
||||||
|
public IReadOnlyList<LootedItemInfo> LootedItems => _lootedItems;
|
||||||
|
|
||||||
|
public void Enable()
|
||||||
|
{
|
||||||
|
if (_isEnabled) return;
|
||||||
|
|
||||||
|
_isEnabled = true;
|
||||||
|
_lootedItems.Clear();
|
||||||
|
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Disable()
|
||||||
|
{
|
||||||
|
if (!_isEnabled) return;
|
||||||
|
|
||||||
|
_isEnabled = false;
|
||||||
|
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
||||||
|
_lootedItems.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_lootedItems.Clear();
|
||||||
|
OnLootedItemsChanged?.Invoke(_lootedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RemoveByIndex(int index)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _lootedItems.Count; i++)
|
||||||
|
{
|
||||||
|
if (_lootedItems[i].Index == index)
|
||||||
|
{
|
||||||
|
_lootedItems.RemoveAt(i);
|
||||||
|
OnLootedItemsChanged?.Invoke(_lootedItems);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
Disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnInventoryChangedRaw(IReadOnlyCollection<InventoryEventArgs> events)
|
||||||
|
{
|
||||||
|
if (!_isEnabled) return;
|
||||||
|
if (!Services.ClientState.IsLoggedIn) return;
|
||||||
|
|
||||||
|
bool hasChanges = false;
|
||||||
|
|
||||||
|
foreach (var eventData in events)
|
||||||
|
{
|
||||||
|
if (!StandardInventories.Contains((InventoryType)eventData.Item.ContainerType)) continue;
|
||||||
|
|
||||||
|
if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs)) continue;
|
||||||
|
|
||||||
|
if (eventData is InventoryItemChangedArgs changedArgs &&
|
||||||
|
changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var inventoryItem = (InventoryItem*)eventData.Item.Address;
|
||||||
|
var changeAmount = eventData is InventoryItemChangedArgs changed
|
||||||
|
? changed.Item.Quantity - changed.OldItemState.Quantity
|
||||||
|
: eventData.Item.Quantity;
|
||||||
|
|
||||||
|
_lootedItems.Add(new LootedItemInfo(
|
||||||
|
_lootedItems.Count,
|
||||||
|
*inventoryItem,
|
||||||
|
changeAmount));
|
||||||
|
|
||||||
|
hasChanges = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges)
|
||||||
|
{
|
||||||
|
OnLootedItemsChanged?.Invoke(_lootedItems);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,19 +180,21 @@ public static unsafe class InventoryScanner
|
|||||||
return InventoryLocation.Invalid;
|
return InventoryLocation.Invalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string GetEmptySlotsString(InventorySourceType source)
|
public static int GetEmptySlots(InventorySourceType source) => (int)(source switch
|
||||||
{
|
|
||||||
int total = InventorySourceDefinitions.GetTotalSlots(source);
|
|
||||||
uint empty = source switch
|
|
||||||
{
|
{
|
||||||
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
|
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
|
||||||
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
|
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
|
||||||
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
|
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
|
||||||
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
|
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
|
||||||
InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer),
|
InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer),
|
||||||
_ => 0,
|
_ => 0u,
|
||||||
};
|
});
|
||||||
uint used = (uint)total - empty;
|
|
||||||
|
public static string GetEmptySlotsString(InventorySourceType source)
|
||||||
|
{
|
||||||
|
int total = InventorySourceDefinitions.GetTotalSlots(source);
|
||||||
|
int empty = GetEmptySlots(source);
|
||||||
|
int used = total - empty;
|
||||||
return $"{used}/{total}";
|
return $"{used}/{total}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,130 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using AetherBags.Configuration;
|
|
||||||
using AetherBags.Currency;
|
|
||||||
using AetherBags.Inventory.Categories;
|
|
||||||
using AetherBags.Inventory.Items;
|
|
||||||
using AetherBags.Inventory.Scanning;
|
|
||||||
using Dalamud.Game.Inventory;
|
|
||||||
using Dalamud.Game.Inventory.InventoryEventArgTypes;
|
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
|
||||||
|
|
||||||
namespace AetherBags.Inventory.State;
|
|
||||||
|
|
||||||
public static unsafe class InventoryState
|
|
||||||
{
|
|
||||||
private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
|
|
||||||
|
|
||||||
private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
|
|
||||||
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
|
|
||||||
private static readonly Dictionary<uint, CategoryBucket> BucketsByKey = new(capacity: 256);
|
|
||||||
private static readonly List<uint> SortedCategoryKeys = new(capacity: 256);
|
|
||||||
private static readonly List<CategorizedInventory> AllCategories = new(capacity: 256);
|
|
||||||
private static readonly List<CategorizedInventory> FilteredCategories = new(capacity: 256);
|
|
||||||
private static readonly List<UserCategoryDefinition> UserCategoriesSortedScratch = new(capacity: 64);
|
|
||||||
private static readonly List<ulong> RemoveKeysScratch = new(capacity: 256);
|
|
||||||
private static readonly HashSet<ulong> ClaimedKeys = new(capacity: 512);
|
|
||||||
private static readonly List<LootedItemInfo>? LootedItems = new(capacity: 512);
|
|
||||||
|
|
||||||
public static bool TrackLootedItems = false;
|
|
||||||
|
|
||||||
public static bool Contains(this IReadOnlyCollection<InventoryType> inventoryTypes, GameInventoryType type)
|
|
||||||
=> inventoryTypes.Contains((InventoryType)type);
|
|
||||||
|
|
||||||
public static IReadOnlyList<CategorizedInventory> GetInventoryItemCategories(string filterString = "", bool invert = false)
|
|
||||||
{
|
|
||||||
return InventoryFilter.FilterCategories(
|
|
||||||
AllCategories,
|
|
||||||
BucketsByKey,
|
|
||||||
FilteredCategories,
|
|
||||||
filterString,
|
|
||||||
invert);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static InventoryStats GetInventoryStats()
|
|
||||||
{
|
|
||||||
int totalItems = ItemInfoByKey.Count;
|
|
||||||
int totalQuantity = 0;
|
|
||||||
|
|
||||||
foreach (var kvp in ItemInfoByKey)
|
|
||||||
{
|
|
||||||
totalQuantity += kvp.Value.ItemCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint emptySlots = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag();
|
|
||||||
const int totalSlots = 140;
|
|
||||||
|
|
||||||
var categories = GetInventoryItemCategories(string.Empty);
|
|
||||||
int categoryCount = categories.Count;
|
|
||||||
|
|
||||||
return new InventoryStats
|
|
||||||
{
|
|
||||||
TotalItems = totalItems,
|
|
||||||
TotalQuantity = totalQuantity,
|
|
||||||
EmptySlots = (int)emptySlots,
|
|
||||||
TotalSlots = totalSlots,
|
|
||||||
CategoryCount = categoryCount,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
|
||||||
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
|
||||||
|
|
||||||
public static void InvalidateCurrencyCaches()
|
|
||||||
=> CurrencyState.InvalidateCaches();
|
|
||||||
|
|
||||||
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
|
|
||||||
=> InventoryScanner.GetInventoryContainer(inventoryType);
|
|
||||||
|
|
||||||
internal static void OnRawItemAdded(IReadOnlyCollection<InventoryEventArgs> events)
|
|
||||||
{
|
|
||||||
if (!TrackLootedItems) return;
|
|
||||||
|
|
||||||
bool updateRequested = false;
|
|
||||||
|
|
||||||
foreach (var eventData in events)
|
|
||||||
{
|
|
||||||
if (!StandardInventories.Contains(eventData.Item.ContainerType)) continue;
|
|
||||||
|
|
||||||
if (!Services.ClientState.IsLoggedIn) return;
|
|
||||||
if (eventData is not (InventoryItemAddedArgs or InventoryItemChangedArgs)) return;
|
|
||||||
if (eventData is InventoryItemChangedArgs changedArgs && changedArgs.OldItemState.Quantity >= changedArgs.Item.Quantity) return;
|
|
||||||
|
|
||||||
var inventoryItem = (InventoryItem*)eventData.Item.Address;
|
|
||||||
var changeAmount = eventData is InventoryItemChangedArgs changed ? changed.Item.Quantity - changed.OldItemState.Quantity : eventData.Item.Quantity;
|
|
||||||
|
|
||||||
LootedItems?.Add(new LootedItemInfo(
|
|
||||||
LootedItems.Count,
|
|
||||||
*inventoryItem,
|
|
||||||
changeAmount)
|
|
||||||
);
|
|
||||||
|
|
||||||
updateRequested = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (updateRequested)
|
|
||||||
{
|
|
||||||
// System.AddonInventoryWindow?.UpdateLootedCategory(LootedItems ?? []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static 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();
|
|
||||||
LootedItems?.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
|
using AetherBags.Currency;
|
||||||
using AetherBags.Inventory.Categories;
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
@@ -165,6 +166,38 @@ public abstract class InventoryStateBase
|
|||||||
|
|
||||||
public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType);
|
public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType);
|
||||||
|
|
||||||
|
public InventoryStats GetStats()
|
||||||
|
{
|
||||||
|
int totalItems = ItemInfoByKey.Count;
|
||||||
|
int totalQuantity = 0;
|
||||||
|
|
||||||
|
foreach (var kvp in ItemInfoByKey)
|
||||||
|
{
|
||||||
|
totalQuantity += kvp.Value.ItemCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalSlots = InventorySourceDefinitions.GetTotalSlots(SourceType);
|
||||||
|
int emptySlots = InventoryScanner.GetEmptySlots(SourceType);
|
||||||
|
|
||||||
|
var categories = GetCategories(string.Empty);
|
||||||
|
int categoryCount = categories.Count;
|
||||||
|
|
||||||
|
return new InventoryStats
|
||||||
|
{
|
||||||
|
TotalItems = totalItems,
|
||||||
|
TotalQuantity = totalQuantity,
|
||||||
|
EmptySlots = emptySlots,
|
||||||
|
TotalSlots = totalSlots,
|
||||||
|
CategoryCount = categoryCount,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
||||||
|
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
||||||
|
|
||||||
|
public static void InvalidateCurrencyCaches()
|
||||||
|
=> CurrencyState.InvalidateCaches();
|
||||||
|
|
||||||
protected virtual void ClearAll()
|
protected virtual void ClearAll()
|
||||||
{
|
{
|
||||||
AggByKey.Clear();
|
AggByKey.Clear();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using AetherBags. Inventory.Scanning;
|
using AetherBags. Inventory.Scanning;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
|
|
||||||
namespace AetherBags. Inventory.State;
|
namespace AetherBags. Inventory.State;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
using AetherBags.Addons;
|
using AetherBags.Addons;
|
||||||
using KamiToolKit.Nodes;
|
|
||||||
using KamiToolKit.Premade.Nodes;
|
using KamiToolKit.Premade.Nodes;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Configuration.Category;
|
namespace AetherBags.Nodes.Configuration.Category;
|
||||||
|
|||||||
@@ -3,15 +3,12 @@ using System.Collections.Generic;
|
|||||||
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;
|
||||||
using KamiToolKit.Classes;
|
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
using Lumina.Excel;
|
using Lumina.Excel;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using Lumina.Text;
|
|
||||||
using Action = System.Action;
|
using Action = System.Action;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Configuration.Category;
|
namespace AetherBags.Nodes.Configuration.Category;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using AetherBags.Nodes.Layout;
|
using AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Inventory;
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
@@ -6,12 +6,19 @@ public sealed class InventoryCategoryHoverCoordinator
|
|||||||
{
|
{
|
||||||
private InventoryCategoryNode? _active;
|
private InventoryCategoryNode? _active;
|
||||||
private int _activeRowIndex = -1;
|
private int _activeRowIndex = -1;
|
||||||
|
private bool _isProcessing;
|
||||||
|
|
||||||
public void OnCategoryHoverChanged(
|
public void OnCategoryHoverChanged(
|
||||||
WrappingGridNode<InventoryCategoryNode> grid,
|
WrappingGridNode<InventoryCategoryNodeBase> grid,
|
||||||
InventoryCategoryNode source,
|
InventoryCategoryNode source,
|
||||||
bool hovering)
|
bool hovering)
|
||||||
{
|
{
|
||||||
|
if (_isProcessing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_isProcessing = true;
|
||||||
grid.RecalculateLayout();
|
grid.RecalculateLayout();
|
||||||
|
|
||||||
if (hovering)
|
if (hovering)
|
||||||
@@ -59,23 +66,34 @@ public sealed class InventoryCategoryHoverCoordinator
|
|||||||
|
|
||||||
_activeRowIndex = -1;
|
_activeRowIndex = -1;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_isProcessing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void ResetAll(WrappingGridNode<InventoryCategoryNode> grid)
|
public void ResetAll(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||||
{
|
{
|
||||||
_active = null;
|
_active = null;
|
||||||
_activeRowIndex = -1;
|
_activeRowIndex = -1;
|
||||||
ClearAll(grid);
|
ClearAll(grid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ClearAll(WrappingGridNode<InventoryCategoryNode> grid)
|
private static void ClearAll(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||||
{
|
{
|
||||||
foreach (var cat in grid.GetNodes<InventoryCategoryNode>())
|
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||||
|
{
|
||||||
|
if (node is InventoryCategoryNode cat)
|
||||||
cat.SetHeaderSuppressed(false);
|
cat.SetHeaderSuppressed(false);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void SuppressAllExcept(WrappingGridNode<InventoryCategoryNode> grid, InventoryCategoryNode source)
|
private static void SuppressAllExcept(WrappingGridNode<InventoryCategoryNodeBase> grid, InventoryCategoryNode source)
|
||||||
{
|
{
|
||||||
foreach (var cat in grid.GetNodes<InventoryCategoryNode>())
|
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||||
|
{
|
||||||
|
if (node is InventoryCategoryNode cat)
|
||||||
cat.SetHeaderSuppressed(!ReferenceEquals(cat, source));
|
cat.SetHeaderSuppressed(!ReferenceEquals(cat, source));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Hooks;
|
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Categories;
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
@@ -9,15 +8,17 @@ using AetherBags.Nodes.Layout;
|
|||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
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.Client.UI.Misc;
|
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Inventory;
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
public class InventoryCategoryNode : SimpleComponentNode
|
public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||||
{
|
{
|
||||||
|
private const uint CategoryNodeKeyBase = 0x10000000;
|
||||||
|
|
||||||
|
public override uint Key => CategoryNodeKeyBase | CategorizedInventory.Key;
|
||||||
private readonly TextNode _categoryNameTextNode;
|
private readonly TextNode _categoryNameTextNode;
|
||||||
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
||||||
|
|
||||||
@@ -109,7 +110,7 @@ public class InventoryCategoryNode : SimpleComponentNode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsPinnedInConfig => CategorizedInventory.Category?.IsPinned ?? false;
|
public override bool IsPinnedInConfig => CategorizedInventory.Category?.IsPinned ?? false;
|
||||||
|
|
||||||
public void BeginHeaderHover()
|
public void BeginHeaderHover()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for category-like nodes that can be displayed in the inventory grid.
|
||||||
|
/// Used to allow both regular categories and special categories (like looted items) to be hoisted/pinned.
|
||||||
|
/// </summary>
|
||||||
|
public abstract class InventoryCategoryNodeBase : SimpleComponentNode
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Unique key for this category, used for sync operations.
|
||||||
|
/// </summary>
|
||||||
|
public abstract uint Key { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this category should be pinned in the layout.
|
||||||
|
/// </summary>
|
||||||
|
public virtual bool IsPinnedInConfig => false;
|
||||||
|
}
|
||||||
@@ -4,13 +4,13 @@ namespace AetherBags.Nodes.Inventory;
|
|||||||
|
|
||||||
public sealed class InventoryCategoryPinCoordinator
|
public sealed class InventoryCategoryPinCoordinator
|
||||||
{
|
{
|
||||||
public bool ApplyPinnedStates(WrappingGridNode<InventoryCategoryNode> grid)
|
public bool ApplyPinnedStates(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||||
{
|
{
|
||||||
bool changed = false;
|
bool changed = false;
|
||||||
|
|
||||||
using (grid.DeferRecalculateLayout())
|
using (grid.DeferRecalculateLayout())
|
||||||
{
|
{
|
||||||
foreach (var node in grid.GetNodes<InventoryCategoryNode>())
|
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||||
{
|
{
|
||||||
bool shouldBePinned = node.IsPinnedInConfig;
|
bool shouldBePinned = node.IsPinnedInConfig;
|
||||||
|
|
||||||
@@ -38,7 +38,7 @@ public sealed class InventoryCategoryPinCoordinator
|
|||||||
return changed;
|
return changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PrunePinnedNotInGrid(WrappingGridNode<InventoryCategoryNode> grid)
|
public bool PrunePinnedNotInGrid(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Currency;
|
using AetherBags.Currency;
|
||||||
using AetherBags.Inventory;
|
|
||||||
using AetherBags.Inventory.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;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
using static AetherBags.Inventory.State.InventoryStateBase;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Inventory;
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
public sealed class InventoryFooterNode : SimpleComponentNode
|
public sealed class InventoryFooterNode : SimpleComponentNode
|
||||||
@@ -44,7 +44,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode
|
|||||||
{
|
{
|
||||||
_currencyListNode.IsVisible = System.Config.Currency.Enabled;
|
_currencyListNode.IsVisible = System.Config.Currency.Enabled;
|
||||||
|
|
||||||
IReadOnlyList<CurrencyInfo> currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
|
IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
|
||||||
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
|
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
|
||||||
dataList: currencyInfoList,
|
dataList: currencyInfoList,
|
||||||
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
using System;
|
||||||
|
using System.Numerics;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using KamiToolKit.Classes;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A display-only item node for looted items. Not draggable, but shows tooltip and can be dismissed.
|
||||||
|
/// </summary>
|
||||||
|
public unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||||
|
{
|
||||||
|
private readonly IconNode _iconNode;
|
||||||
|
private readonly TextNode _quantityTextNode;
|
||||||
|
private readonly ResNode _collisionNode;
|
||||||
|
|
||||||
|
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
||||||
|
|
||||||
|
public LootedItemDisplayNode()
|
||||||
|
{
|
||||||
|
Size = new Vector2(42, 46);
|
||||||
|
|
||||||
|
_iconNode = new IconNode
|
||||||
|
{
|
||||||
|
Position = new Vector2(0, 0),
|
||||||
|
Size = new Vector2(42, 46),
|
||||||
|
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled,
|
||||||
|
};
|
||||||
|
_iconNode.AttachNode(this);
|
||||||
|
|
||||||
|
_quantityTextNode = new TextNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(40.0f, 12.0f),
|
||||||
|
Position = new Vector2(4.0f, 34.0f),
|
||||||
|
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents,
|
||||||
|
Color = ColorHelper.GetColor(50),
|
||||||
|
TextOutlineColor = ColorHelper.GetColor(51),
|
||||||
|
TextFlags = TextFlags.Edge,
|
||||||
|
AlignmentType = AlignmentType.Right,
|
||||||
|
};
|
||||||
|
_quantityTextNode.AttachNode(this);
|
||||||
|
|
||||||
|
_collisionNode = new ResNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(42, 46),
|
||||||
|
NodeFlags = NodeFlags.Enabled | NodeFlags.EmitsEvents | NodeFlags.HasCollision,
|
||||||
|
};
|
||||||
|
_collisionNode.AddEvent(AtkEventType.MouseOver, OnMouseOver);
|
||||||
|
_collisionNode.AddEvent(AtkEventType.MouseOut, OnMouseOut);
|
||||||
|
_collisionNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||||
|
_collisionNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public LootedItemInfo LootedItem { get; private set; } = null!;
|
||||||
|
|
||||||
|
public void SetLootedItem(LootedItemInfo lootedItem)
|
||||||
|
{
|
||||||
|
LootedItem = lootedItem;
|
||||||
|
var item = lootedItem.Item;
|
||||||
|
_iconNode.IconId = item.IconId;
|
||||||
|
_quantityTextNode.String = lootedItem.Quantity > 1 ? lootedItem.Quantity.ToString() : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseOver(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
var item = LootedItem.Item;
|
||||||
|
_collisionNode.ShowInventoryItemTooltip(item.Container, item.Slot);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseOut(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(_collisionNode)->Id;
|
||||||
|
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseClick(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
if (!atkEventData->IsLeftClick) return;
|
||||||
|
OnDismiss?.Invoke(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
using AetherBags.Nodes.Layout;
|
||||||
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A special category node for displaying recently looted items.
|
||||||
|
/// Items are not draggable but can be dismissed individually or cleared entirely.
|
||||||
|
/// </summary>
|
||||||
|
public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||||
|
{
|
||||||
|
private const uint LootedCategoryKey = 0x20000001;
|
||||||
|
|
||||||
|
public override uint Key => LootedCategoryKey;
|
||||||
|
private readonly TextNode _headerTextNode;
|
||||||
|
private readonly CircleButtonNode _clearButton;
|
||||||
|
private readonly HybridDirectionalFlexNode<LootedItemDisplayNode> _itemGridNode;
|
||||||
|
|
||||||
|
private const float HeaderHeight = 20;
|
||||||
|
private const float ClearButtonSize = 20;
|
||||||
|
private const float MinWidth = 100;
|
||||||
|
|
||||||
|
private IReadOnlyList<LootedItemInfo> _lootedItems = Array.Empty<LootedItemInfo>();
|
||||||
|
|
||||||
|
private int _hoverRefs;
|
||||||
|
private bool _headerExpanded;
|
||||||
|
private float _baseHeaderWidth = 96f;
|
||||||
|
private string _fullHeaderText = "Recently Looted";
|
||||||
|
|
||||||
|
public event Action<LootedItemsCategoryNode, bool>? HeaderHoverChanged;
|
||||||
|
public Action<int>? OnDismissItem { get; set; }
|
||||||
|
public Action? OnClearAll { get; set; }
|
||||||
|
|
||||||
|
public int ItemsPerLine
|
||||||
|
{
|
||||||
|
get => _itemGridNode.ItemsPerLine;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_itemGridNode.ItemsPerLine == value) return;
|
||||||
|
_itemGridNode.ItemsPerLine = value;
|
||||||
|
RecalculateSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool HasItems => _lootedItems.Count > 0;
|
||||||
|
|
||||||
|
public LootedItemsCategoryNode()
|
||||||
|
{
|
||||||
|
_headerTextNode = new TextNode
|
||||||
|
{
|
||||||
|
Position = Vector2.Zero,
|
||||||
|
Size = new Vector2(96, HeaderHeight),
|
||||||
|
AlignmentType = AlignmentType.Left,
|
||||||
|
String = "Recently Looted",
|
||||||
|
TextFlags = TextFlags.OverflowHidden | TextFlags.Ellipsis,
|
||||||
|
TextColor = new Vector4(0.9f, 0.8f, 0.5f, 1.0f), // Gold-ish color
|
||||||
|
};
|
||||||
|
|
||||||
|
_headerTextNode.AddEvent(AtkEventType.MouseOver, BeginHeaderHover);
|
||||||
|
_headerTextNode.AddEvent(AtkEventType.MouseOut, EndHeaderHover);
|
||||||
|
|
||||||
|
_headerTextNode.TextFlags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||||
|
_headerTextNode.TextFlags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
|
||||||
|
|
||||||
|
_headerTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
|
||||||
|
_headerTextNode.AttachNode(this);
|
||||||
|
|
||||||
|
_clearButton = new CircleButtonNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(ClearButtonSize),
|
||||||
|
Icon = ButtonIcon.CrossSmall,
|
||||||
|
OnClick = () => OnClearAll?.Invoke(),
|
||||||
|
};
|
||||||
|
_clearButton.AttachNode(this);
|
||||||
|
|
||||||
|
_itemGridNode = new HybridDirectionalFlexNode<LootedItemDisplayNode>
|
||||||
|
{
|
||||||
|
Position = new Vector2(0, HeaderHeight),
|
||||||
|
Size = new Vector2(240, 92),
|
||||||
|
FillRowsFirst = true,
|
||||||
|
ItemsPerLine = 10,
|
||||||
|
HorizontalPadding = 5,
|
||||||
|
VerticalPadding = 2,
|
||||||
|
};
|
||||||
|
_itemGridNode.NodeFlags |= NodeFlags.EmitsEvents;
|
||||||
|
_itemGridNode.AttachNode(this);
|
||||||
|
|
||||||
|
RecalculateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateLootedItems(IReadOnlyList<LootedItemInfo> lootedItems)
|
||||||
|
{
|
||||||
|
_lootedItems = lootedItems;
|
||||||
|
UpdateHeaderText();
|
||||||
|
SyncItemGrid();
|
||||||
|
RecalculateSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateHeaderText()
|
||||||
|
{
|
||||||
|
_fullHeaderText = _lootedItems.Count > 0
|
||||||
|
? $"Recently Looted ({_lootedItems.Count})"
|
||||||
|
: "Recently Looted";
|
||||||
|
|
||||||
|
_headerTextNode.String = _fullHeaderText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void BeginHeaderHover()
|
||||||
|
{
|
||||||
|
_hoverRefs++;
|
||||||
|
if (_hoverRefs != 1) return;
|
||||||
|
|
||||||
|
_headerExpanded = true;
|
||||||
|
ApplyHeaderVisualStateAndSize();
|
||||||
|
HeaderHoverChanged?.Invoke(this, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void EndHeaderHover()
|
||||||
|
{
|
||||||
|
if (_hoverRefs <= 0) return;
|
||||||
|
|
||||||
|
_hoverRefs--;
|
||||||
|
if (_hoverRefs != 0) return;
|
||||||
|
|
||||||
|
_headerExpanded = false;
|
||||||
|
ApplyHeaderVisualStateAndSize();
|
||||||
|
HeaderHoverChanged?.Invoke(this, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyHeaderVisualStateAndSize()
|
||||||
|
{
|
||||||
|
var flags = _headerTextNode.TextFlags;
|
||||||
|
flags &= ~(TextFlags.WordWrap | TextFlags.MultiLine);
|
||||||
|
|
||||||
|
if (_headerExpanded)
|
||||||
|
{
|
||||||
|
flags &= ~(TextFlags.OverflowHidden | TextFlags.Ellipsis);
|
||||||
|
_headerTextNode.TextFlags = flags;
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_fullHeaderText))
|
||||||
|
_headerTextNode.String = _fullHeaderText;
|
||||||
|
|
||||||
|
Vector2 drawSize = _headerTextNode.GetTextDrawSize();
|
||||||
|
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
|
||||||
|
_headerTextNode.Size = _headerTextNode.Size with { X = expandedWidth };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_headerTextNode.Size = _headerTextNode.Size with { X = _baseHeaderWidth };
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(_fullHeaderText))
|
||||||
|
_headerTextNode.String = _fullHeaderText;
|
||||||
|
|
||||||
|
flags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||||
|
_headerTextNode.TextFlags = flags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncItemGrid()
|
||||||
|
{
|
||||||
|
_itemGridNode.SyncWithListData(
|
||||||
|
_lootedItems,
|
||||||
|
node => node.LootedItem,
|
||||||
|
CreateLootedItemNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||||
|
{
|
||||||
|
var node = new LootedItemDisplayNode
|
||||||
|
{
|
||||||
|
OnDismiss = OnItemDismissed,
|
||||||
|
};
|
||||||
|
node.SetLootedItem(lootedItem);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnItemDismissed(LootedItemDisplayNode node)
|
||||||
|
{
|
||||||
|
int index = node.LootedItem.Index;
|
||||||
|
OnDismissItem?.Invoke(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecalculateSize()
|
||||||
|
{
|
||||||
|
int itemCount = _lootedItems.Count;
|
||||||
|
|
||||||
|
if (itemCount == 0)
|
||||||
|
{
|
||||||
|
float width = MinWidth;
|
||||||
|
Size = new Vector2(width, HeaderHeight);
|
||||||
|
_baseHeaderWidth = width - ClearButtonSize - 4;
|
||||||
|
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||||
|
_clearButton.Position = new Vector2(width - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||||
|
_clearButton.IsVisible = false;
|
||||||
|
_itemGridNode.Position = new Vector2(0, HeaderHeight);
|
||||||
|
_itemGridNode.Size = new Vector2(width, 0);
|
||||||
|
ApplyHeaderVisualStateAndSize();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
|
||||||
|
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
|
||||||
|
int actualColumns = Math.Min(itemCount, itemsPerLine);
|
||||||
|
|
||||||
|
const float cellW = 42f;
|
||||||
|
const float cellH = 46f;
|
||||||
|
|
||||||
|
float hPad = _itemGridNode.HorizontalPadding;
|
||||||
|
float vPad = _itemGridNode.VerticalPadding;
|
||||||
|
|
||||||
|
float calculatedWidth = Math.Max(MinWidth, actualColumns * cellW + (actualColumns - 1) * hPad);
|
||||||
|
float gridHeight = rows * cellH + (rows - 1) * vPad;
|
||||||
|
float totalHeight = HeaderHeight + gridHeight;
|
||||||
|
|
||||||
|
Size = new Vector2(calculatedWidth, totalHeight);
|
||||||
|
_baseHeaderWidth = calculatedWidth - ClearButtonSize - 4;
|
||||||
|
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||||
|
_clearButton.Position = new Vector2(calculatedWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||||
|
_clearButton.IsVisible = true;
|
||||||
|
_itemGridNode.Position = new Vector2(0, HeaderHeight);
|
||||||
|
_itemGridNode.Size = new Vector2(calculatedWidth, gridHeight);
|
||||||
|
ApplyHeaderVisualStateAndSize();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
using System. Numerics;
|
using System. Numerics;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Inventory;
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|||||||
@@ -6,16 +6,13 @@ using AetherBags.Helpers;
|
|||||||
using AetherBags.Hooks;
|
using AetherBags.Hooks;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
using AetherBags.Inventory.State;
|
|
||||||
using AetherBags.IPC;
|
using AetherBags.IPC;
|
||||||
using Dalamud.Game.Gui;
|
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
|
||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
|
|
||||||
namespace AetherBags;
|
namespace AetherBags;
|
||||||
|
|
||||||
public unsafe class Plugin : IDalamudPlugin
|
public class Plugin : IDalamudPlugin
|
||||||
{
|
{
|
||||||
private readonly CommandHandler _commandHandler;
|
private readonly CommandHandler _commandHandler;
|
||||||
private readonly InventoryHooks _inventoryHooks;
|
private readonly InventoryHooks _inventoryHooks;
|
||||||
@@ -32,6 +29,7 @@ public unsafe class Plugin : IDalamudPlugin
|
|||||||
KamiToolKitLibrary.Initialize(pluginInterface);
|
KamiToolKitLibrary.Initialize(pluginInterface);
|
||||||
|
|
||||||
System.IPC = new IPCService();
|
System.IPC = new IPCService();
|
||||||
|
System.LootedItemsTracker = new LootedItemsTracker();
|
||||||
|
|
||||||
System.AddonInventoryWindow = new AddonInventoryWindow
|
System.AddonInventoryWindow = new AddonInventoryWindow
|
||||||
{
|
{
|
||||||
@@ -83,6 +81,7 @@ public unsafe class Plugin : IDalamudPlugin
|
|||||||
_inventoryHooks.Dispose();
|
_inventoryHooks.Dispose();
|
||||||
_inventoryLifecycles.Dispose();
|
_inventoryLifecycles.Dispose();
|
||||||
|
|
||||||
|
System.LootedItemsTracker.Dispose();
|
||||||
System.IPC.Dispose();
|
System.IPC.Dispose();
|
||||||
HighlightState.ClearAll();
|
HighlightState.ClearAll();
|
||||||
|
|
||||||
@@ -98,7 +97,7 @@ public unsafe class Plugin : IDalamudPlugin
|
|||||||
private void OnLogin()
|
private void OnLogin()
|
||||||
{
|
{
|
||||||
System.Config = Util.LoadConfigOrDefault();
|
System.Config = Util.LoadConfigOrDefault();
|
||||||
InventoryState.TrackLootedItems = true;
|
System.LootedItemsTracker.Enable();
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
System.AddonInventoryWindow.Toggle();
|
System.AddonInventoryWindow.Toggle();
|
||||||
@@ -109,7 +108,7 @@ public unsafe class Plugin : IDalamudPlugin
|
|||||||
private void OnLogout(int type, int code)
|
private void OnLogout(int type, int code)
|
||||||
{
|
{
|
||||||
Util.SaveConfig(System.Config);
|
Util.SaveConfig(System.Config);
|
||||||
InventoryState.TrackLootedItems = false;
|
System.LootedItemsTracker.Disable();
|
||||||
System.AddonInventoryWindow.Close();
|
System.AddonInventoryWindow.Close();
|
||||||
System.AddonSaddleBagWindow.Close();
|
System.AddonSaddleBagWindow.Close();
|
||||||
System.AddonRetainerWindow.Close();
|
System.AddonRetainerWindow.Close();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using AetherBags.Addons;
|
using AetherBags.Addons;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
|
using AetherBags.Inventory;
|
||||||
using AetherBags.IPC;
|
using AetherBags.IPC;
|
||||||
|
|
||||||
namespace AetherBags;
|
namespace AetherBags;
|
||||||
@@ -12,4 +13,5 @@ public static class System
|
|||||||
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
||||||
public static IPCService IPC { get; set; } = null!;
|
public static IPCService IPC { get; set; } = null!;
|
||||||
public static SystemConfiguration Config { get; set; } = null!;
|
public static SystemConfiguration Config { get; set; } = null!;
|
||||||
|
public static LootedItemsTracker LootedItemsTracker { get; set; } = null!;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user