Pinning, Hoisting, Recently Lotted
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Context;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.Inventory.State;
|
||||
using AetherBags.Nodes.Input;
|
||||
using AetherBags.Nodes.Inventory;
|
||||
@@ -15,6 +17,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
{
|
||||
private readonly MainBagState _inventoryState = new();
|
||||
private InventoryNotificationNode _notificationNode = null!;
|
||||
private LootedItemsCategoryNode _lootedCategoryNode = null!;
|
||||
|
||||
protected override InventoryStateBase InventoryState => _inventoryState;
|
||||
|
||||
@@ -22,7 +25,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
{
|
||||
InitializeBackgroundDropTarget();
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
@@ -33,6 +36,13 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
};
|
||||
CategoriesNode.AttachNode(this);
|
||||
|
||||
_lootedCategoryNode = new LootedItemsCategoryNode
|
||||
{
|
||||
ItemsPerLine = 10,
|
||||
OnDismissItem = OnDismissLootedItem,
|
||||
OnClearAll = OnClearAllLootedItems,
|
||||
};
|
||||
|
||||
var header = CalculateHeaderLayout(addon);
|
||||
|
||||
_notificationNode = new InventoryNotificationNode
|
||||
@@ -71,14 +81,69 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||
|
||||
_isSetupComplete = true;
|
||||
System.LootedItemsTracker.OnLootedItemsChanged += OnLootedItemsChanged;
|
||||
|
||||
IsSetupComplete = true;
|
||||
|
||||
_inventoryState.RefreshFromGame();
|
||||
|
||||
var existingLoot = System.LootedItemsTracker.LootedItems;
|
||||
if (existingLoot.Count > 0)
|
||||
{
|
||||
UpdateLootedCategory(existingLoot);
|
||||
}
|
||||
|
||||
RefreshCategoriesCore(autosize: true);
|
||||
|
||||
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()
|
||||
{
|
||||
if (!Services.ClientState.IsLoggedIn) return;
|
||||
@@ -95,6 +160,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
protected override void OnFinalize(AtkUnitBase* addon)
|
||||
{
|
||||
System.LootedItemsTracker.OnLootedItemsChanged -= OnLootedItemsChanged;
|
||||
|
||||
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
|
||||
if (blockingAddonId != 0)
|
||||
{
|
||||
@@ -103,7 +170,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||
|
||||
_isSetupComplete = false;
|
||||
IsSetupComplete = false;
|
||||
base.OnFinalize(addon);
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
@@ -107,7 +107,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
LayoutContent();
|
||||
|
||||
_inventoryState.RefreshFromGame();
|
||||
_isSetupComplete = true;
|
||||
IsSetupComplete = true;
|
||||
|
||||
RefreshCategoriesCore(autosize: true);
|
||||
|
||||
@@ -116,7 +116,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
protected override void RefreshCategoriesCore(bool autosize)
|
||||
{
|
||||
if (!_isSetupComplete)
|
||||
if (!IsSetupComplete)
|
||||
return;
|
||||
|
||||
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
||||
@@ -179,7 +179,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
protected override void OnFinalize(AtkUnitBase* addon)
|
||||
{
|
||||
_isSetupComplete = false;
|
||||
IsSetupComplete = false;
|
||||
|
||||
CloseRetainerWindows();
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
@@ -80,7 +80,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
_inventoryState.RefreshFromGame();
|
||||
|
||||
_isSetupComplete = true;
|
||||
IsSetupComplete = true;
|
||||
|
||||
RefreshCategoriesCore(autosize: true);
|
||||
|
||||
@@ -89,7 +89,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
protected override void RefreshCategoriesCore(bool autosize)
|
||||
{
|
||||
if (!_isSetupComplete)
|
||||
if (!IsSetupComplete)
|
||||
return;
|
||||
|
||||
_slotCounterNode.String = _inventoryState.GetEmptySlotsString();
|
||||
@@ -99,7 +99,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
protected override void OnFinalize(AtkUnitBase* addon)
|
||||
{
|
||||
_isSetupComplete = false;
|
||||
IsSetupComplete = false;
|
||||
|
||||
if (System.Config.General.HideGameSaddleBags)
|
||||
{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using KamiToolKit.Premade;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using AetherBags.Inventory.Items;
|
||||
|
||||
namespace AetherBags.Addons;
|
||||
|
||||
public interface IInventoryWindow
|
||||
@@ -8,4 +10,5 @@ public interface IInventoryWindow
|
||||
void ManualRefresh();
|
||||
void ItemRefresh();
|
||||
void SetSearchText(string searchText);
|
||||
InventoryStats GetStats();
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using AetherBags.Helpers;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.Inventory.Scanning;
|
||||
using AetherBags.Inventory.State;
|
||||
using AetherBags.Nodes.Input;
|
||||
@@ -27,7 +28,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
||||
|
||||
protected DragDropNode BackgroundDropTarget = null!;
|
||||
protected WrappingGridNode<InventoryCategoryNode> CategoriesNode = null!;
|
||||
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
||||
protected TextInputWithButtonNode SearchInputNode = null!;
|
||||
protected InventoryFooterNode FooterNode = null!;
|
||||
protected TextNode? SlotCounterNode { get; set; }
|
||||
@@ -49,8 +50,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
protected bool RefreshQueued;
|
||||
protected bool RefreshAutosizeQueued;
|
||||
private bool _isRefreshing;
|
||||
protected bool _isSetupComplete;
|
||||
protected bool IsSetupComplete;
|
||||
|
||||
protected abstract InventoryStateBase InventoryState { get; }
|
||||
|
||||
@@ -59,13 +59,14 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected virtual bool HasSlotCounter => false;
|
||||
|
||||
private readonly HashSet<uint> _searchMatchScratch = new();
|
||||
private bool _isRefreshing;
|
||||
|
||||
public void ManualRefresh()
|
||||
{
|
||||
if (!IsOpen) return;
|
||||
if (!Services.ClientState.IsLoggedIn) return;
|
||||
if (_isRefreshing) return;
|
||||
if (!_isSetupComplete) return;
|
||||
if (!IsSetupComplete) return;
|
||||
|
||||
try
|
||||
{
|
||||
@@ -82,6 +83,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty;
|
||||
|
||||
public InventoryStats GetStats() => InventoryState.GetStats();
|
||||
|
||||
public virtual void SetSearchText(string searchText)
|
||||
{
|
||||
Services.Framework.RunOnTick(() =>
|
||||
@@ -93,7 +96,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
public void RefreshFromLifecycle()
|
||||
{
|
||||
if (!_isSetupComplete) return;
|
||||
if (!IsSetupComplete) return;
|
||||
if (!IsOpen) return;
|
||||
if (_isRefreshing) return;
|
||||
|
||||
@@ -111,7 +114,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
protected virtual void RefreshCategoriesCore(bool autosize)
|
||||
{
|
||||
if (!_isSetupComplete)
|
||||
if (!IsSetupComplete)
|
||||
return;
|
||||
|
||||
var config = System.Config.General;
|
||||
@@ -166,7 +169,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
||||
dataList: categories,
|
||||
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
||||
getKeyFromNode: node => node.CategorizedInventory.Key,
|
||||
getKeyFromNode: node => node.Key,
|
||||
updateNode: (node, data) =>
|
||||
{
|
||||
node.CategorizedInventory = data;
|
||||
@@ -394,7 +397,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected void ResizeWindow(float width, float height)
|
||||
=> 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)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Context;
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AetherBags.Addons;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.State;
|
||||
using AetherBags.Inventory.Items;
|
||||
using Dalamud.Game.Command;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
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 subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty;
|
||||
@@ -88,8 +89,7 @@ public class CommandHandler : IDisposable
|
||||
|
||||
case "count":
|
||||
case "stats":
|
||||
var stats = InventoryState.GetInventoryStats();
|
||||
PrintChat($"{stats.UsedSlots}/{stats.TotalSlots} slots used ({stats.UsagePercent:F0}%) | {stats.TotalItems} unique items | {stats.CategoryCount} categories");
|
||||
PrintInventoryStats();
|
||||
break;
|
||||
|
||||
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)
|
||||
{
|
||||
if (!System.AddonInventoryWindow.IsOpen)
|
||||
|
||||
@@ -4,8 +4,36 @@ public class SystemConfiguration
|
||||
{
|
||||
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 CategorySettings Categories { get; set; } = new();
|
||||
public CurrencySettings Currency { get; set; } = new();
|
||||
public GeneralSettings General
|
||||
{
|
||||
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.Linq;
|
||||
using AetherBags;
|
||||
using Dalamud.Game.Addon.Lifecycle;
|
||||
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
|
||||
using Dalamud.Plugin.Services;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
|
||||
namespace AetherBags.Extensions;
|
||||
|
||||
public static class AddonLifecycleExtensions {
|
||||
extension(IAddonLifecycle addonLifecycle) {
|
||||
public void LogAddon(string addonName, params AddonEvent[] loggedModules) {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
|
||||
using AetherBags.Inventory;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using Lumina.Text.ReadOnly;
|
||||
using Lumina.Text;
|
||||
|
||||
namespace AetherBags.Extensions;
|
||||
|
||||
public static unsafe class DragDropPayloadExtensions
|
||||
public static class DragDropPayloadExtensions
|
||||
{
|
||||
extension(DragDropPayload payload)
|
||||
{
|
||||
|
||||
@@ -8,10 +8,16 @@ public static class LoggerExtensions
|
||||
{
|
||||
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.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
|
||||
|
||||
@@ -87,11 +87,17 @@ public static class Util
|
||||
private static SystemConfiguration LoadConfig()
|
||||
{
|
||||
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()
|
||||
=> LoadConfig() ?? new SystemConfiguration();
|
||||
{
|
||||
var config = LoadConfig() ?? new SystemConfiguration();
|
||||
config.EnsureInitialized();
|
||||
return config;
|
||||
}
|
||||
|
||||
public static SystemConfiguration ResetConfig()
|
||||
=> new SystemConfiguration();
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
using Dalamud.Plugin;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
|
||||
namespace AetherBags.IPC;
|
||||
|
||||
|
||||
@@ -8,30 +8,45 @@ namespace AetherBags.Inventory;
|
||||
public static unsafe class InventoryOrchestrator
|
||||
{
|
||||
private static readonly InventoryNotificationState NotificationState = new();
|
||||
private static bool _isRefreshing;
|
||||
|
||||
public static void RefreshAll(bool updateMaps = true)
|
||||
{
|
||||
if (updateMaps)
|
||||
if (_isRefreshing)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
InventoryContextState.RefreshMaps();
|
||||
InventoryContextState.RefreshBlockedSlots();
|
||||
}
|
||||
_isRefreshing = true;
|
||||
|
||||
var agent = AgentInventory.Instance();
|
||||
var contextId = agent != null ? agent->OpenTitleId : 0;
|
||||
var notification = NotificationState.GetNotificationInfo(contextId);
|
||||
|
||||
Services.Framework.RunOnTick(() =>
|
||||
{
|
||||
if (System.AddonInventoryWindow.IsOpen)
|
||||
System.AddonInventoryWindow.SetNotification(notification!);
|
||||
|
||||
foreach (var window in GetAllWindows())
|
||||
if (updateMaps)
|
||||
{
|
||||
if (window.IsOpen)
|
||||
window.ManualRefresh();
|
||||
InventoryContextState.RefreshMaps();
|
||||
InventoryContextState.RefreshBlockedSlots();
|
||||
}
|
||||
});
|
||||
|
||||
if (!HasAnyWindowOpen())
|
||||
return;
|
||||
|
||||
var agent = AgentInventory.Instance();
|
||||
var contextId = agent != null ? agent->OpenTitleId : 0;
|
||||
var notification = NotificationState.GetNotificationInfo(contextId);
|
||||
|
||||
Services.Framework.RunOnTick(() =>
|
||||
{
|
||||
if (notification != null && System.AddonInventoryWindow.IsOpen)
|
||||
System.AddonInventoryWindow.SetNotification(notification);
|
||||
|
||||
foreach (var window in GetAllWindows())
|
||||
{
|
||||
window.ManualRefresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRefreshing = false;
|
||||
}
|
||||
}
|
||||
|
||||
public static void CloseAll()
|
||||
@@ -44,6 +59,9 @@ public static unsafe class InventoryOrchestrator
|
||||
|
||||
public static void RefreshHighlights()
|
||||
{
|
||||
if (!HasAnyWindowOpen())
|
||||
return;
|
||||
|
||||
Services.Framework.RunOnTick(() =>
|
||||
{
|
||||
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()
|
||||
{
|
||||
yield return System.AddonInventoryWindow;
|
||||
yield return System.AddonSaddleBagWindow;
|
||||
yield return System.AddonRetainerWindow;
|
||||
if (System.AddonInventoryWindow != null)
|
||||
yield return System.AddonInventoryWindow;
|
||||
if (System.AddonSaddleBagWindow != null)
|
||||
yield return System.AddonSaddleBagWindow;
|
||||
if (System.AddonRetainerWindow != null)
|
||||
yield return System.AddonRetainerWindow;
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,13 @@ public readonly struct InventoryStats
|
||||
public int CategoryCount { get; init; }
|
||||
public int UsedSlots => TotalSlots - EmptySlots;
|
||||
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;
|
||||
}
|
||||
|
||||
public static int GetEmptySlots(InventorySourceType source) => (int)(source switch
|
||||
{
|
||||
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
|
||||
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
|
||||
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
|
||||
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
|
||||
InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer),
|
||||
_ => 0u,
|
||||
});
|
||||
|
||||
public static string GetEmptySlotsString(InventorySourceType source)
|
||||
{
|
||||
int total = InventorySourceDefinitions.GetTotalSlots(source);
|
||||
uint empty = source switch
|
||||
{
|
||||
InventorySourceType.MainBags => InventoryManager.Instance()->GetEmptySlotsInBag(),
|
||||
InventorySourceType.SaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.SaddleBag),
|
||||
InventorySourceType.PremiumSaddleBag => GetEmptySlotsInContainer(InventorySourceDefinitions.PremiumSaddleBag),
|
||||
InventorySourceType.AllSaddleBags => GetEmptySlotsInContainer(InventorySourceDefinitions.AllSaddleBags),
|
||||
InventorySourceType.Retainer => GetEmptySlotsInContainer(InventorySourceDefinitions.Retainer),
|
||||
_ => 0,
|
||||
};
|
||||
uint used = (uint)total - empty;
|
||||
int empty = GetEmptySlots(source);
|
||||
int used = total - empty;
|
||||
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.Linq;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Currency;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.Items;
|
||||
@@ -165,6 +166,38 @@ public abstract class InventoryStateBase
|
||||
|
||||
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()
|
||||
{
|
||||
AggByKey.Clear();
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using AetherBags. Inventory.Scanning;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
|
||||
namespace AetherBags. Inventory.State;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
using System;
|
||||
using AetherBags.Addons;
|
||||
using KamiToolKit.Nodes;
|
||||
using KamiToolKit.Premade.Nodes;
|
||||
|
||||
namespace AetherBags.Nodes.Configuration.Category;
|
||||
|
||||
@@ -3,15 +3,12 @@ using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Nodes.Color;
|
||||
using Dalamud.Utility;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
using Lumina.Text;
|
||||
using Action = System.Action;
|
||||
|
||||
namespace AetherBags.Nodes.Configuration.Category;
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Inventory;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using AetherBags.Nodes.Layout;
|
||||
using AetherBags.Nodes.Layout;
|
||||
|
||||
namespace AetherBags.Nodes.Inventory;
|
||||
|
||||
@@ -6,76 +6,94 @@ public sealed class InventoryCategoryHoverCoordinator
|
||||
{
|
||||
private InventoryCategoryNode? _active;
|
||||
private int _activeRowIndex = -1;
|
||||
private bool _isProcessing;
|
||||
|
||||
public void OnCategoryHoverChanged(
|
||||
WrappingGridNode<InventoryCategoryNode> grid,
|
||||
WrappingGridNode<InventoryCategoryNodeBase> grid,
|
||||
InventoryCategoryNode source,
|
||||
bool hovering)
|
||||
{
|
||||
grid.RecalculateLayout();
|
||||
if (_isProcessing)
|
||||
return;
|
||||
|
||||
if (hovering)
|
||||
try
|
||||
{
|
||||
_active = source;
|
||||
_isProcessing = true;
|
||||
grid.RecalculateLayout();
|
||||
|
||||
if (!grid.TryGetRowIndex(source, out _activeRowIndex))
|
||||
if (hovering)
|
||||
{
|
||||
SuppressAllExcept(grid, source);
|
||||
_active = source;
|
||||
|
||||
if (!grid.TryGetRowIndex(source, out _activeRowIndex))
|
||||
{
|
||||
SuppressAllExcept(grid, source);
|
||||
source.SetHeaderSuppressed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearAll(grid);
|
||||
|
||||
var row = grid.Rows[_activeRowIndex];
|
||||
for (int i = 0; i < row.Count; i++)
|
||||
{
|
||||
if (row[i] is InventoryCategoryNode cat && !ReferenceEquals(cat, source))
|
||||
cat.SetHeaderSuppressed(true);
|
||||
}
|
||||
|
||||
source.SetHeaderSuppressed(false);
|
||||
return;
|
||||
}
|
||||
|
||||
ClearAll(grid);
|
||||
if (!ReferenceEquals(_active, source))
|
||||
return;
|
||||
|
||||
var row = grid.Rows[_activeRowIndex];
|
||||
for (int i = 0; i < row.Count; i++)
|
||||
_active = null;
|
||||
|
||||
if (_activeRowIndex >= 0 && _activeRowIndex < grid.Rows.Count)
|
||||
{
|
||||
if (row[i] is InventoryCategoryNode cat && !ReferenceEquals(cat, source))
|
||||
cat.SetHeaderSuppressed(true);
|
||||
var row = grid.Rows[_activeRowIndex];
|
||||
for (int i = 0; i < row.Count; i++)
|
||||
{
|
||||
if (row[i] is InventoryCategoryNode cat)
|
||||
cat.SetHeaderSuppressed(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearAll(grid);
|
||||
}
|
||||
|
||||
source.SetHeaderSuppressed(false);
|
||||
return;
|
||||
_activeRowIndex = -1;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(_active, source))
|
||||
return;
|
||||
|
||||
_active = null;
|
||||
|
||||
if (_activeRowIndex >= 0 && _activeRowIndex < grid.Rows.Count)
|
||||
finally
|
||||
{
|
||||
var row = grid.Rows[_activeRowIndex];
|
||||
for (int i = 0; i < row.Count; i++)
|
||||
{
|
||||
if (row[i] is InventoryCategoryNode cat)
|
||||
cat.SetHeaderSuppressed(false);
|
||||
}
|
||||
_isProcessing = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ClearAll(grid);
|
||||
}
|
||||
|
||||
_activeRowIndex = -1;
|
||||
}
|
||||
|
||||
public void ResetAll(WrappingGridNode<InventoryCategoryNode> grid)
|
||||
public void ResetAll(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||
{
|
||||
_active = null;
|
||||
_activeRowIndex = -1;
|
||||
ClearAll(grid);
|
||||
}
|
||||
|
||||
private static void ClearAll(WrappingGridNode<InventoryCategoryNode> grid)
|
||||
private static void ClearAll(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||
{
|
||||
foreach (var cat in grid.GetNodes<InventoryCategoryNode>())
|
||||
cat.SetHeaderSuppressed(false);
|
||||
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||
{
|
||||
if (node is InventoryCategoryNode cat)
|
||||
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>())
|
||||
cat.SetHeaderSuppressed(!ReferenceEquals(cat, source));
|
||||
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||
{
|
||||
if (node is InventoryCategoryNode cat)
|
||||
cat.SetHeaderSuppressed(!ReferenceEquals(cat, source));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Numerics;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Hooks;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Items;
|
||||
@@ -9,15 +8,17 @@ using AetherBags.Nodes.Layout;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
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 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()
|
||||
{
|
||||
|
||||
@@ -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 bool ApplyPinnedStates(WrappingGridNode<InventoryCategoryNode> grid)
|
||||
public bool ApplyPinnedStates(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||
{
|
||||
bool changed = false;
|
||||
|
||||
using (grid.DeferRecalculateLayout())
|
||||
{
|
||||
foreach (var node in grid.GetNodes<InventoryCategoryNode>())
|
||||
foreach (var node in grid.GetNodes<InventoryCategoryNodeBase>())
|
||||
{
|
||||
bool shouldBePinned = node.IsPinnedInConfig;
|
||||
|
||||
@@ -38,7 +38,7 @@ public sealed class InventoryCategoryPinCoordinator
|
||||
return changed;
|
||||
}
|
||||
|
||||
public bool PrunePinnedNotInGrid(WrappingGridNode<InventoryCategoryNode> grid)
|
||||
public bool PrunePinnedNotInGrid(WrappingGridNode<InventoryCategoryNodeBase> grid)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Items;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AetherBags.Currency;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.State;
|
||||
using AetherBags.Nodes.Currency;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
using static AetherBags.Inventory.State.InventoryStateBase;
|
||||
|
||||
namespace AetherBags.Nodes.Inventory;
|
||||
|
||||
public sealed class InventoryFooterNode : SimpleComponentNode
|
||||
@@ -44,7 +44,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode
|
||||
{
|
||||
_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>(
|
||||
dataList: currencyInfoList,
|
||||
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Context;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
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 FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
|
||||
namespace AetherBags.Nodes.Inventory;
|
||||
|
||||
@@ -6,16 +6,13 @@ using AetherBags.Helpers;
|
||||
using AetherBags.Hooks;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.State;
|
||||
using AetherBags.IPC;
|
||||
using Dalamud.Game.Gui;
|
||||
using Dalamud.Plugin;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using KamiToolKit;
|
||||
|
||||
namespace AetherBags;
|
||||
|
||||
public unsafe class Plugin : IDalamudPlugin
|
||||
public class Plugin : IDalamudPlugin
|
||||
{
|
||||
private readonly CommandHandler _commandHandler;
|
||||
private readonly InventoryHooks _inventoryHooks;
|
||||
@@ -32,6 +29,7 @@ public unsafe class Plugin : IDalamudPlugin
|
||||
KamiToolKitLibrary.Initialize(pluginInterface);
|
||||
|
||||
System.IPC = new IPCService();
|
||||
System.LootedItemsTracker = new LootedItemsTracker();
|
||||
|
||||
System.AddonInventoryWindow = new AddonInventoryWindow
|
||||
{
|
||||
@@ -83,6 +81,7 @@ public unsafe class Plugin : IDalamudPlugin
|
||||
_inventoryHooks.Dispose();
|
||||
_inventoryLifecycles.Dispose();
|
||||
|
||||
System.LootedItemsTracker.Dispose();
|
||||
System.IPC.Dispose();
|
||||
HighlightState.ClearAll();
|
||||
|
||||
@@ -98,7 +97,7 @@ public unsafe class Plugin : IDalamudPlugin
|
||||
private void OnLogin()
|
||||
{
|
||||
System.Config = Util.LoadConfigOrDefault();
|
||||
InventoryState.TrackLootedItems = true;
|
||||
System.LootedItemsTracker.Enable();
|
||||
|
||||
#if DEBUG
|
||||
System.AddonInventoryWindow.Toggle();
|
||||
@@ -109,7 +108,7 @@ public unsafe class Plugin : IDalamudPlugin
|
||||
private void OnLogout(int type, int code)
|
||||
{
|
||||
Util.SaveConfig(System.Config);
|
||||
InventoryState.TrackLootedItems = false;
|
||||
System.LootedItemsTracker.Disable();
|
||||
System.AddonInventoryWindow.Close();
|
||||
System.AddonSaddleBagWindow.Close();
|
||||
System.AddonRetainerWindow.Close();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using AetherBags.Addons;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.IPC;
|
||||
|
||||
namespace AetherBags;
|
||||
@@ -12,4 +13,5 @@ public static class System
|
||||
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
||||
public static IPCService IPC { get; set; } = null!;
|
||||
public static SystemConfiguration Config { get; set; } = null!;
|
||||
public static LootedItemsTracker LootedItemsTracker { get; set; } = null!;
|
||||
}
|
||||
Reference in New Issue
Block a user