diff --git a/AetherBags.sln.DotSettings.user b/AetherBags.sln.DotSettings.user
deleted file mode 100644
index 6ef2c30..0000000
--- a/AetherBags.sln.DotSettings.user
+++ /dev/null
@@ -1,10 +0,0 @@
-
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
- ForceIncluded
\ No newline at end of file
diff --git a/AetherBags/AddonLifecycles/InventoryLifecycles.cs b/AetherBags/AddonLifecycles/InventoryLifecycles.cs
index 04f3352..d35639b 100644
--- a/AetherBags/AddonLifecycles/InventoryLifecycles.cs
+++ b/AetherBags/AddonLifecycles/InventoryLifecycles.cs
@@ -2,12 +2,16 @@ using System;
using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
+using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.NativeWrapper;
using Dalamud.Utility;
+using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.ReadOnly;
+using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace AetherBags.AddonLifecycles;
@@ -16,10 +20,90 @@ public class InventoryLifecycles : IDisposable
public InventoryLifecycles()
{
- Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], PreRefreshHandler);
+ var bags = new[] { "Inventory", "InventoryLarge", "InventoryExpansion" };
+ var saddle = new[] { "InventoryBuddy" };
+ var retainer = new[] { "InventoryRetainer", "InventoryRetainerLarge" };
+
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, saddle, OnPostSetup);
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostSetup, retainer, OnPostSetup);
+
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, saddle, OnPreFinalize);
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
+
+ // PreRefresh Handlers
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"], InventoryPreRefreshHandler);
+
+ // PostRequestedUpdate
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate);
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate);
+
+ // PreShow
+ Services.AddonLifecycle.RegisterListener(AddonEvent.PreOpen, "InventoryBuddy", OnSaddleBagOpen);
+
Services.Logger.Verbose("InventoryLifecycles initialized");
}
+ private void OnPreFinalize(AddonEvent type, AddonArgs args)
+ {
+ CloseInventories(args.AddonName);
+ }
+
+ private void OnPostSetup(AddonEvent type, AddonArgs args)
+ {
+ OpenInventories(args.AddonName);
+ }
+
+ private unsafe void OpenInventories(string name)
+ {
+ GeneralSettings config = System.Config.General;
+ if (name.Contains("Retainer") && config.OpenRetainerWithGameInventory)
+ {
+ System.AddonRetainerWindow.Open();
+ if (config.HideGameRetainer)
+ {
+ var addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryRetainer");
+ if (addon != null)
+ {
+ addon->IsVisible = false;
+ }
+
+ addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryRetainerLarge");
+ if (addon != null)
+ {
+ addon->IsVisible = false;
+ }
+ }
+ }
+
+ if (name.Contains("InventoryBuddy") && config.OpenSaddleBagsWithGameInventory)
+ {
+ System.AddonSaddleBagWindow.Open();
+ if (config.HideGameSaddleBags)
+ {
+ var addon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryBuddy");
+ if (addon != null)
+ {
+ addon->IsVisible = false;
+ }
+ }
+ }
+ }
+
+ private void CloseInventories(string name)
+ {
+ if (name.Contains("Retainer")) System.AddonRetainerWindow.Close();
+ if (name.Contains("InventoryBuddy")) System.AddonSaddleBagWindow.Close();
+ }
+
+ private static bool IsInUnsafeState()
+ {
+ if (!Services.ClientState.IsLoggedIn)
+ return true;
+
+ return Services.Condition.Any(ConditionFlag.BetweenAreas, ConditionFlag.BetweenAreas51);
+ }
+
/*
values[0] = OpenType
values[1] = OpenTitleId
@@ -31,14 +115,17 @@ public class InventoryLifecycles : IDisposable
values[7] = can use Saddlebags (Agent InventoryBuddy IsActivatable)
*/
- private unsafe void PreRefreshHandler(AddonEvent type, AddonArgs args)
+ private unsafe void InventoryPreRefreshHandler(AddonEvent type, AddonArgs args)
{
if (args is not AddonRefreshArgs refreshArgs)
return;
+ if (IsInUnsafeState())
+ return;
+
GeneralSettings config = System.Config.General;
- Services.Logger.Debug("PreRefresh event for Inventory detected");
+ Services.Logger.DebugOnly("PreRefresh event for Inventory detected");
AtkValuePtr[] atkValues = refreshArgs.AtkValueEnumerable.ToArray();
@@ -48,6 +135,9 @@ public class InventoryLifecycles : IDisposable
AtkValue* value5 = (AtkValue*)atkValues[5].Address;
AtkValue* value6 = (AtkValue*)atkValues[6].Address;
+ if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString)
+ return;
+
int openTitleId = value1->Int;
ReadOnlySeString title = value5->String.AsReadOnlySeString();
ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString();
@@ -68,8 +158,71 @@ public class InventoryLifecycles : IDisposable
}
}
+ // TODO: Inventory/Retainers are not perma open, need some way to close it too.
+ private void InventoryBuddyPreRefreshHandler(AddonEvent type, AddonArgs args)
+ {
+ if (args is not AddonRefreshArgs refreshArgs)
+ return;
+
+ if (IsInUnsafeState())
+ return;
+
+ GeneralSettings config = System.Config.General;
+
+ if (config.HideGameSaddleBags) refreshArgs.AtkValueCount = 0;
+ if (config.OpenSaddleBagsWithGameInventory)
+ {
+ System.AddonSaddleBagWindow.Toggle();
+ }
+ }
+
+
+ private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (IsInUnsafeState())
+ return;
+
+ System.AddonInventoryWindow?.RefreshFromLifecycle();
+ }
+
+ private void OnSaddleBagUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (IsInUnsafeState())
+ return;
+
+ System.AddonSaddleBagWindow?.RefreshFromLifecycle();
+ }
+
+ private void OnRetainerInventoryUpdate(AddonEvent type, AddonArgs args)
+ {
+ if (IsInUnsafeState())
+ return;
+
+ System.AddonRetainerWindow?.RefreshFromLifecycle();
+ }
+
+ private void OnSaddleBagOpen(AddonEvent type, AddonArgs args)
+ {
+ if (args is not AddonShowArgs showArgs)
+ return;
+ }
+
public void Dispose()
{
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, "InventoryBuddy", OnPostSetup);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PostSetup, "InventoryRetainer, InventoryRetainerLarge", OnPostSetup);
+
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "InventoryBuddy", OnPreFinalize);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PreFinalize, "InventoryRetainer, InventoryRetainerLarge", OnPreFinalize);
+
Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["Inventory", "InventoryLarge", "InventoryExpansion"]);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["InventoryBuddy"]);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PreRefresh, ["InventoryRetainer", "InventoryRetainerLarge"]);
+
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, "InventoryBuddy", OnSaddleBagUpdate);
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PostRequestedUpdate, ["InventoryRetainer", "InventoryRetainerLarge"], OnRetainerInventoryUpdate);
+
+ Services.AddonLifecycle.UnregisterListener(AddonEvent.PreShow, ["InventoryBuddy"], OnSaddleBagOpen);
}
}
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs
index 4d46049..484e1e4 100644
--- a/AetherBags/Addons/AddonCategoryConfigurationWindow.cs
+++ b/AetherBags/Addons/AddonCategoryConfigurationWindow.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using AetherBags.Configuration;
+using AetherBags.Inventory;
using AetherBags.Nodes.Configuration.Category;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
@@ -77,7 +78,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
.ToList();
}
- private void OnOptionChanged(CategoryWrapper? newOption)
+ private void OnOptionChanged(CategoryWrapper? newOption)
{
if (_configNode is null) return;
@@ -118,7 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
listNode.AddOption(newWrapper);
RefreshSelectionList();
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
private void OnRemoveCategory(CategoryWrapper categoryWrapper)
@@ -134,7 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
{
OnOptionChanged(null);
}
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
private void RefreshSelectionList()
diff --git a/AetherBags/Addons/AddonConfigurationWindow.cs b/AetherBags/Addons/AddonConfigurationWindow.cs
index 85e6692..bf95edd 100644
--- a/AetherBags/Addons/AddonConfigurationWindow.cs
+++ b/AetherBags/Addons/AddonConfigurationWindow.cs
@@ -37,8 +37,6 @@ public class AddonConfigurationWindow : NativeAddon
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
- ContentHeight = 400,
- ScrollSpeed = 25,
IsVisible = true,
};
_generalScrollingAreaNode.AttachNode(this);
@@ -47,8 +45,6 @@ public class AddonConfigurationWindow : NativeAddon
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
- ContentHeight = 400,
- ScrollSpeed = 25,
IsVisible = false,
};
_categoryScrollingAreaNode.AttachNode(this);
@@ -57,8 +53,6 @@ public class AddonConfigurationWindow : NativeAddon
{
Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight },
- ContentHeight = 400,
- ScrollSpeed = 25,
IsVisible = false,
};
_currencyScrollingAreaNode.AttachNode(this);
diff --git a/AetherBags/Addons/AddonInventoryWindow.cs b/AetherBags/Addons/AddonInventoryWindow.cs
index faf0d95..8348b7b 100644
--- a/AetherBags/Addons/AddonInventoryWindow.cs
+++ b/AetherBags/Addons/AddonInventoryWindow.cs
@@ -1,53 +1,28 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
using System.Numerics;
-using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
+using AetherBags.Inventory.State;
using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout;
-using Dalamud.Game.Addon.Lifecycle;
-using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit;
using KamiToolKit.Nodes;
namespace AetherBags.Addons;
-public class AddonInventoryWindow : NativeAddon
+public unsafe class AddonInventoryWindow : InventoryAddonBase
{
- private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new();
- private readonly InventoryCategoryPinCoordinator _pinCoordinator = new();
- private readonly HashSet _hoverSubscribed = new();
-
+ private readonly MainBagState _inventoryState = new();
private InventoryNotificationNode _notificationNode = null!;
- private WrappingGridNode _categoriesNode = null!;
- private TextInputWithHintNode _searchInputNode = null!;
- private CircleButtonNode _settingsButtonNode = null!;
- private InventoryFooterNode _footerNode = null!;
- // Window constraints
- private const float MinWindowWidth = 300;
- private const float MaxWindowWidth = 800;
- private const float MinWindowHeight = 200;
- private const float MaxWindowHeight = 1000;
+ protected override InventoryStateBase InventoryState => _inventoryState;
- // Layout settings
- private const float CategorySpacing = 12;
- private const float ItemSize = 40;
- private const float ItemPadding = 4;
-
- private const float FooterHeight = 28f;
- private const float FooterTopSpacing = 4f;
-
- private bool _refreshQueued;
- private bool _refreshAutosizeQueued;
-
- protected override unsafe void OnSetup(AtkUnitBase* addon)
+ protected override void OnSetup(AtkUnitBase* addon)
{
- _categoriesNode = new WrappingGridNode
+ InitializeBackgroundDropTarget();
+
+ CategoriesNode = new WrappingGridNode
{
Position = ContentStartPosition,
Size = ContentSize,
@@ -56,276 +31,69 @@ public class AddonInventoryWindow : NativeAddon
TopPadding = 4.0f,
BottomPadding = 4.0f,
};
- _categoriesNode.AttachNode(this);
+ CategoriesNode.AttachNode(this);
- var size = new Vector2(addon->Size.X / 2.0f, 28.0f);
-
- var header = addon->WindowHeaderCollisionNode;
-
- float headerX = header->X;
- float headerY = header->Y;
- float headerW = header->Width;
- float headerH = header->Height;
-
- float x = headerX + (headerW - size.X) * 0.5f;
- float y = headerY + (headerH - size.Y) * 0.5f;
+ var header = CalculateHeaderLayout(addon);
_notificationNode = new InventoryNotificationNode
{
Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f),
- Size = new Vector2(headerW, 28f),
+ Size = new Vector2(header.HeaderWidth, 28f),
};
_notificationNode.AttachNode(this);
- _searchInputNode = new TextInputWithHintNode
+ SearchInputNode = new TextInputWithButtonNode
{
- Position = new Vector2(x, y),
- Size = size,
- OnInputReceived = _ => RefreshCategoriesCore(autosize: false),
+ Position = header.SearchPosition,
+ Size = header.SearchSize,
+ OnInputReceived = _ => ItemRefresh(),
+ OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this)
};
- _searchInputNode.AttachNode(this);
+ SearchInputNode.AttachNode(this);
- _settingsButtonNode = new CircleButtonNode
+ SettingsButtonNode = new CircleButtonNode
{
- Position = new Vector2(headerW - 48f, y),
+ Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY),
Size = new Vector2(28f),
Icon = ButtonIcon.GearCog,
OnClick = System.AddonConfigurationWindow.Toggle
};
- _settingsButtonNode.AttachNode(this);
+ SettingsButtonNode.AttachNode(this);
- _footerNode = new InventoryFooterNode
+ FooterNode = new InventoryFooterNode
{
Size = ContentSize with { Y = FooterHeight },
- SlotAmountText = InventoryState.GetEmptyItemSlotsString(),
+ SlotAmountText = _inventoryState.GetEmptySlotsString(),
};
- _footerNode.AttachNode(this);
+ FooterNode.AttachNode(this);
LayoutContent();
- Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
- InventoryState.RefreshFromGame();
+ _isSetupComplete = true;
+ _inventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true);
base.OnSetup(addon);
}
- protected override unsafe void OnUpdate(AtkUnitBase* addon)
- {
- if (_refreshQueued)
- {
- bool doAutosize = _refreshAutosizeQueued;
- _refreshQueued = false;
- _refreshAutosizeQueued = false;
-
- RefreshCategoriesCore(doAutosize);
- }
-
- base.OnUpdate(addon);
- }
-
- public void ManualInventoryRefresh()
- {
- if (!Services.ClientState.IsLoggedIn) return;
- InventoryState.RefreshFromGame();
- RefreshCategoriesCore(true);
- }
-
- /*public void UpdateLootedCategory(IReadOnlyList lootedItemInfos)
- {
- if (!Services.ClientState.IsLoggedIn) return;
- _recentlyLootedCategoryNode?.CategorizedInventory.Items.AddRange(
- lootedItemInfos.Select(x => new ItemInfo
- {
- ItemCount = x.Quantity,
- Key = uint.MaxValue - 1,
- Item = x.Item,
- })
- .ToList());
- RefreshCategoriesCore(true);
- }*/
-
public void ManualCurrencyRefresh()
{
if (!Services.ClientState.IsLoggedIn) return;
- _footerNode.RefreshCurrencies();
+ FooterNode.RefreshCurrencies();
}
- private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
- {
- InventoryState.RefreshFromGame();
-
- RefreshCategoriesCore(autosize: true);
- }
-
- protected override unsafe void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
- {
- base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
-
- InventoryState.RefreshFromGame();
-
- RefreshCategoriesCore(autosize: true);
- }
-
- private void RefreshCategoriesCore(bool autosize)
- {
- _footerNode.SlotAmountText = InventoryState.GetEmptyItemSlotsString();
- _footerNode.RefreshCurrencies();
-
- string filter = _searchInputNode.SearchString.ExtractText();
- IReadOnlyList categories = InventoryState.GetInventoryItemCategories(filter);
-
- float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
- int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
-
- _categoriesNode.SyncWithListDataByKey(
- dataList: categories,
- getKeyFromData: c => c.Key,
- getKeyFromNode: n => n.CategorizedInventory.Key,
- updateNode: (node, data) =>
- {
- node.CategorizedInventory = data;
- node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
- },
- createNodeMethod: _ => new InventoryCategoryNode
- {
- Size = ContentSize with { Y = 120 },
- });
-
- bool pinsChanged = _pinCoordinator.ApplyPinnedStates(_categoriesNode);
- if (pinsChanged)
- _hoverCoordinator.ResetAll(_categoriesNode);
-
- WireHoverHandlers();
-
- if (autosize) AutoSizeWindow();
- else
- {
- LayoutContent();
- _categoriesNode.RecalculateLayout();
- }
- }
-
-
- private void WireHoverHandlers()
- {
- var nodes = _categoriesNode.Nodes;
-
- for (int i = 0; i < nodes.Count; i++)
- {
- if (nodes[i] is not InventoryCategoryNode node)
- continue;
-
- if (!_hoverSubscribed.Add(node))
- continue;
-
- node.HeaderHoverChanged += (src, hovering) =>
- {
- _hoverCoordinator.OnCategoryHoverChanged(_categoriesNode, src, hovering);
- };
- }
- }
-
- private int CalculateOptimalItemsPerLine(float availableWidth)
- {
- return Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
- }
-
- private void LayoutContent()
- {
- Vector2 contentPos = ContentStartPosition;
- Vector2 contentSize = ContentSize;
-
- float footerH = FooterHeight;
-
- _footerNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH);
- _footerNode.Size = new Vector2(contentSize.X, footerH);
-
- float gridH = contentSize.Y - footerH - FooterTopSpacing;
- if (gridH < 0) gridH = 0;
-
- _categoriesNode.Position = contentPos;
- _categoriesNode.Size = new Vector2(contentSize.X, gridH);
- }
-
- private void AutoSizeWindow()
- {
- var nodes = _categoriesNode.Nodes;
-
- float maxChildWidth = 0f;
- int childCount = 0;
-
- for (int i = 0; i < nodes.Count; i++)
- {
- if (nodes[i] is not InventoryCategoryNode cat)
- continue;
-
- childCount++;
- float w = cat.Width;
- if (w > maxChildWidth) maxChildWidth = w;
- }
-
- if (childCount == 0)
- {
- ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
- return;
- }
-
- float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
- float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
-
- float contentWidth = finalWidth - (ContentStartPosition.X * 2);
-
- float gridBudget = Math.Max(0f, MaxWindowHeight - FooterHeight - FooterTopSpacing);
-
- _categoriesNode.Position = ContentStartPosition;
- _categoriesNode.Size = new Vector2(contentWidth, gridBudget);
-
- _categoriesNode.RecalculateLayout();
-
- float requiredGridHeight = _categoriesNode.GetRequiredHeight();
- float requiredContentHeight = requiredGridHeight + FooterTopSpacing + FooterHeight;
-
- float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
- float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
-
- ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
- }
-
- private void ResizeWindow(float width, float height, bool recalcLayout)
- {
- SetWindowSize(width, height);
- LayoutContent();
-
- if (recalcLayout)
- _categoriesNode.RecalculateLayout();
- }
-
- private void ResizeWindow(float width, float height)
- => ResizeWindow(width, height, recalcLayout: true);
-
public void SetNotification(InventoryNotificationInfo info)
{
Services.Framework.RunOnTick(() =>
{
- if(IsOpen) _notificationNode.NotificationInfo = info;
- }, delayTicks: 1);
+ if (IsOpen) _notificationNode.NotificationInfo = info;
+ }, delayTicks: 3);
}
- public void SetSearchText(string searchText)
- {
- Services.Framework.RunOnTick(() =>
- {
- if(IsOpen) _searchInputNode.SearchString = searchText;
- RefreshCategoriesCore(autosize: true);
- }, delayTicks: 1);
- }
-
-
- protected override unsafe void OnFinalize(AtkUnitBase* addon)
+ protected override void OnFinalize(AtkUnitBase* addon)
{
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
if (blockingAddonId != 0)
@@ -333,13 +101,9 @@ public class AddonInventoryWindow : NativeAddon
RaptureAtkModule.Instance()->CloseAddon(blockingAddonId);
}
- Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
- _hoverSubscribed.Clear();
- _refreshQueued = false;
- _refreshAutosizeQueued = false;
-
+ _isSetupComplete = false;
base.OnFinalize(addon);
}
-}
+}
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonRetainerWindow.cs b/AetherBags/Addons/AddonRetainerWindow.cs
new file mode 100644
index 0000000..f710013
--- /dev/null
+++ b/AetherBags/Addons/AddonRetainerWindow.cs
@@ -0,0 +1,188 @@
+using System.Linq;
+using System.Numerics;
+using AetherBags.Inventory;
+using AetherBags.Inventory.State;
+using AetherBags.Nodes.Input;
+using AetherBags.Nodes.Inventory;
+using AetherBags.Nodes.Layout;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Addons;
+
+public unsafe class AddonRetainerWindow : InventoryAddonBase
+{
+ private readonly RetainerState _inventoryState = new();
+ private TextNode _slotCounterNode = null!;
+ private TextNode _retainerNameNode = null!;
+ private TextButtonNode _entrustDuplicatesButton = null!;
+
+ protected override InventoryStateBase InventoryState => _inventoryState;
+
+ protected override bool HasFooter => false;
+ protected override bool HasSlotCounter => true;
+
+ private readonly Vector3 _tintColor = new(8f / 255f, -8f / 255f, -4f / 255f);
+
+ protected override float MinWindowWidth => 400;
+ protected override float MaxWindowWidth => 700;
+
+ private readonly string[] _retainerAddonNames = { "InventoryRetainer", "InventoryRetainerLarge" };
+
+ protected override void OnSetup(AtkUnitBase* addon)
+ {
+ InitializeBackgroundDropTarget();
+
+ WindowNode?.AddColor = _tintColor;
+
+ CategoriesNode = new WrappingGridNode
+ {
+ Position = ContentStartPosition,
+ Size = ContentSize,
+ HorizontalSpacing = CategorySpacing,
+ VerticalSpacing = CategorySpacing,
+ TopPadding = 4.0f,
+ BottomPadding = 4.0f,
+ };
+ CategoriesNode.AttachNode(this);
+
+ var header = CalculateHeaderLayout(addon);
+
+ SearchInputNode = new TextInputWithButtonNode
+ {
+ Position = header.SearchPosition,
+ Size = header.SearchSize,
+ OnInputReceived = _ => ItemRefresh(),
+ OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this)
+ };
+ SearchInputNode.AttachNode(this);
+
+ SettingsButtonNode = new CircleButtonNode
+ {
+ Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY),
+ Size = new Vector2(28f),
+ Icon = ButtonIcon.GearCog,
+ OnClick = System.AddonConfigurationWindow.Toggle
+ };
+ SettingsButtonNode.AttachNode(this);
+
+ _retainerNameNode = new TextNode
+ {
+ Position = new Vector2(8f, 0),
+ Size = new Vector2(200, 20),
+ AlignmentType = AlignmentType.Left,
+ FontType = FontType.MiedingerMed,
+ TextFlags = TextFlags.Glare,
+ TextColor = ColorHelper.GetColor(50),
+ TextOutlineColor = ColorHelper.GetColor(32),
+ };
+ _retainerNameNode.AttachNode(this);
+
+ _entrustDuplicatesButton = new TextButtonNode
+ {
+ Size = new Vector2(120, 28),
+ AddColor = _tintColor,
+ String = "Entrust Duplicates",
+ OnClick = OnEntrustDuplicates,
+ };
+ _entrustDuplicatesButton.AttachNode(this);
+
+ // Slot counter
+ _slotCounterNode = new TextNode
+ {
+ Position = new Vector2(Size.X - 10, 0),
+ Size = new Vector2(82, 20),
+ AlignmentType = AlignmentType.Right,
+ FontType = FontType.MiedingerMed,
+ TextFlags = TextFlags.Glare,
+ TextColor = ColorHelper.GetColor(50),
+ TextOutlineColor = ColorHelper.GetColor(32),
+ };
+ _slotCounterNode.AttachNode(this);
+ SlotCounterNode = _slotCounterNode;
+
+ LayoutContent();
+
+ _inventoryState.RefreshFromGame();
+ _isSetupComplete = true;
+
+ RefreshCategoriesCore(autosize: true);
+
+ base.OnSetup(addon);
+ }
+
+ protected override void RefreshCategoriesCore(bool autosize)
+ {
+ if (!_isSetupComplete)
+ return;
+
+ _slotCounterNode.String = _inventoryState.GetEmptySlotsString();
+ _retainerNameNode.String = RetainerState.CurrentRetainerName;
+
+ base.RefreshCategoriesCore(autosize);
+ }
+
+ protected override void LayoutContent()
+ {
+ base.LayoutContent();
+
+ Vector2 contentPos = ContentStartPosition;
+ Vector2 contentSize = ContentSize;
+
+ float footerY = contentPos.Y + contentSize.Y - FooterHeight + 4f;
+
+ _retainerNameNode.Position = new Vector2(contentPos.X + 8f, footerY);
+
+ float buttonWidth = _entrustDuplicatesButton.Width;
+ float buttonX = contentPos.X + (contentSize.X - buttonWidth) / 2f;
+ _entrustDuplicatesButton.Position = new Vector2(buttonX, footerY - 2f);
+
+ if (SlotCounterNode != null)
+ SlotCounterNode.Position = new Vector2(contentSize.X - 80f, footerY);
+ }
+
+ private void CloseRetainerWindows()
+ {
+ var manager = RaptureAtkUnitManager.Instance();
+ foreach (var name in _retainerAddonNames)
+ {
+ var addon = manager->GetAddonByName(name);
+ if (addon != null)
+ {
+ addon->IsVisible = true;
+ addon->Close(true);
+ }
+ }
+ }
+
+ private bool IsAnyRetainerWindowLoaded()
+ {
+ return _retainerAddonNames.Any(name => RaptureAtkUnitManager.Instance()->GetAddonByName(name) != null);
+ }
+
+ protected override void OnShow(AtkUnitBase* addon)
+ {
+ base.OnShow(addon);
+
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
+ }
+
+ private void OnEntrustDuplicates()
+ {
+ if (!IsAnyRetainerWindowLoaded()) return;
+ var agent = AgentModule.Instance()->GetAgentByInternalId(AgentId.Retainer);
+ agent->SendCommand(0, [0]);
+ }
+
+ protected override void OnFinalize(AtkUnitBase* addon)
+ {
+ _isSetupComplete = false;
+
+ CloseRetainerWindows();
+
+ base.OnFinalize(addon);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Addons/AddonSaddleBagWindow.cs b/AetherBags/Addons/AddonSaddleBagWindow.cs
new file mode 100644
index 0000000..71ceabc
--- /dev/null
+++ b/AetherBags/Addons/AddonSaddleBagWindow.cs
@@ -0,0 +1,116 @@
+using System.Numerics;
+using AetherBags.Inventory.State;
+using AetherBags.Nodes.Input;
+using AetherBags.Nodes.Inventory;
+using AetherBags.Nodes.Layout;
+using FFXIVClientStructs.FFXIV.Client.UI;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Addons;
+
+public unsafe class AddonSaddleBagWindow : InventoryAddonBase
+{
+ private readonly SaddleBagState _inventoryState = new();
+ private TextNode _slotCounterNode = null!;
+
+ protected override InventoryStateBase InventoryState => _inventoryState;
+
+ protected override bool HasFooter => false;
+ protected override bool HasSlotCounter => true;
+
+ private readonly Vector3 _tintColor = new (-16f / 255f, -4f / 255f, 8f / 255f);
+
+ protected override float MinWindowWidth => 400;
+ protected override float MaxWindowWidth => 600;
+
+ protected override void OnSetup(AtkUnitBase* addon)
+ {
+ InitializeBackgroundDropTarget();
+
+ WindowNode?.AddColor = _tintColor;
+
+ CategoriesNode = new WrappingGridNode
+ {
+ Position = ContentStartPosition,
+ Size = ContentSize,
+ HorizontalSpacing = CategorySpacing,
+ VerticalSpacing = CategorySpacing,
+ TopPadding = 4.0f,
+ BottomPadding = 4.0f,
+ };
+ CategoriesNode.AttachNode(this);
+
+ var header = CalculateHeaderLayout(addon);
+
+ SearchInputNode = new TextInputWithButtonNode
+ {
+ Position = header.SearchPosition,
+ Size = header.SearchSize,
+ OnInputReceived = _ => ItemRefresh(),
+ OnButtonClicked = () => InventoryAddonContextMenu.OpenMain(this)
+ };
+ SearchInputNode.AttachNode(this);
+
+ SettingsButtonNode = new CircleButtonNode
+ {
+ Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY),
+ Size = new Vector2(28f),
+ AddColor = _tintColor,
+ Icon = ButtonIcon.GearCog,
+ OnClick = System.AddonConfigurationWindow.Toggle
+ };
+ SettingsButtonNode.AttachNode(this);
+
+ _slotCounterNode = new TextNode
+ {
+ Position = new Vector2(Size.X - 10, 0),
+ Size = new Vector2(82, 20),
+ AlignmentType = AlignmentType.Right,
+ FontType = FontType.MiedingerMed,
+ TextFlags = TextFlags.Glare,
+ TextColor = ColorHelper.GetColor(50),
+ TextOutlineColor = ColorHelper.GetColor(32)
+ };
+ _slotCounterNode.AttachNode(this);
+ SlotCounterNode = _slotCounterNode;
+
+ LayoutContent();
+
+ _inventoryState.RefreshFromGame();
+
+ _isSetupComplete = true;
+
+ RefreshCategoriesCore(autosize: true);
+
+ base.OnSetup(addon);
+ }
+
+ protected override void RefreshCategoriesCore(bool autosize)
+ {
+ if (!_isSetupComplete)
+ return;
+
+ _slotCounterNode.String = _inventoryState.GetEmptySlotsString();
+
+ base.RefreshCategoriesCore(autosize);
+ }
+
+ protected override void OnFinalize(AtkUnitBase* addon)
+ {
+ _isSetupComplete = false;
+
+ if (System.Config.General.HideGameSaddleBags)
+ {
+ var saddleAddon = RaptureAtkUnitManager.Instance()->GetAddonByName("InventoryBuddy");
+ if (saddleAddon != null)
+ {
+ saddleAddon->IsVisible = true;
+ saddleAddon->Close(true);
+ }
+ }
+
+ base.OnFinalize(addon);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Addons/CategoryWrapper.cs b/AetherBags/Addons/CategoryWrapper.cs
index 790571c..c0e488e 100644
--- a/AetherBags/Addons/CategoryWrapper.cs
+++ b/AetherBags/Addons/CategoryWrapper.cs
@@ -1,5 +1,6 @@
using AetherBags.Configuration;
using AetherBags.Inventory;
+using AetherBags.Inventory.Categories;
using KamiToolKit.Premade;
namespace AetherBags.Addons;
diff --git a/AetherBags/Addons/IInventoryWindow.cs b/AetherBags/Addons/IInventoryWindow.cs
new file mode 100644
index 0000000..b8abd4b
--- /dev/null
+++ b/AetherBags/Addons/IInventoryWindow.cs
@@ -0,0 +1,11 @@
+namespace AetherBags.Addons;
+
+public interface IInventoryWindow
+{
+ bool IsOpen { get; }
+ void Toggle();
+ void Close();
+ void ManualRefresh();
+ void ItemRefresh();
+ void SetSearchText(string searchText);
+}
\ No newline at end of file
diff --git a/AetherBags/Addons/InventoryAddonBase.cs b/AetherBags/Addons/InventoryAddonBase.cs
new file mode 100644
index 0000000..b516d7e
--- /dev/null
+++ b/AetherBags/Addons/InventoryAddonBase.cs
@@ -0,0 +1,437 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using AetherBags.Configuration;
+using AetherBags.Helpers;
+using AetherBags.Inventory;
+using AetherBags.Inventory.Categories;
+using AetherBags.Inventory.Context;
+using AetherBags.Inventory.Scanning;
+using AetherBags.Inventory.State;
+using AetherBags.Nodes.Input;
+using AetherBags.Nodes.Inventory;
+using AetherBags.Nodes.Layout;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit;
+using KamiToolKit.Classes;
+using KamiToolKit.Classes.ContextMenu;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Addons;
+
+public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
+{
+ protected readonly InventoryCategoryHoverCoordinator HoverCoordinator = new();
+ protected readonly InventoryCategoryPinCoordinator PinCoordinator = new();
+ protected readonly HashSet HoverSubscribed = new();
+
+ protected DragDropNode BackgroundDropTarget = null!;
+ protected WrappingGridNode CategoriesNode = null!;
+ protected TextInputWithButtonNode SearchInputNode = null!;
+ protected InventoryFooterNode FooterNode = null!;
+ protected TextNode? SlotCounterNode { get; set; }
+ protected CircleButtonNode SettingsButtonNode = null!;
+
+ internal ContextMenu ContextMenu = null!;
+
+ protected virtual float MinWindowWidth => 600;
+ protected virtual float MaxWindowWidth => 800;
+ protected virtual float MinWindowHeight => 200;
+ protected virtual float MaxWindowHeight => 1000;
+
+ protected const float CategorySpacing = 12;
+ protected const float ItemSize = 40;
+ protected const float ItemPadding = 4;
+ protected const float FooterHeight = 28f;
+ protected const float FooterTopSpacing = 4f;
+ protected const float SettingsButtonOffset = 48f;
+
+ protected bool RefreshQueued;
+ protected bool RefreshAutosizeQueued;
+ private bool _isRefreshing;
+ protected bool _isSetupComplete;
+
+ protected abstract InventoryStateBase InventoryState { get; }
+
+ protected virtual bool HasFooter => true;
+ protected virtual bool HasPinning => true;
+ protected virtual bool HasSlotCounter => false;
+
+ private readonly HashSet _searchMatchScratch = new();
+
+ public void ManualRefresh()
+ {
+ if (!IsOpen) return;
+ if (!Services.ClientState.IsLoggedIn) return;
+ if (_isRefreshing) return;
+ if (!_isSetupComplete) return;
+
+ try
+ {
+ _isRefreshing = true;
+ InventoryState.RefreshFromGame();
+ RefreshCategoriesCore(autosize: true);
+ }
+ finally
+ {
+ _isRefreshing = false;
+ }
+ }
+
+
+ public string GetSearchText() => SearchInputNode?.SearchString.ExtractText() ?? string.Empty;
+
+ public virtual void SetSearchText(string searchText)
+ {
+ Services.Framework.RunOnTick(() =>
+ {
+ if (IsOpen) SearchInputNode.SearchString = searchText;
+ RefreshCategoriesCore(autosize: true);
+ }, delayTicks: 3);
+ }
+
+ public void RefreshFromLifecycle()
+ {
+ if (!_isSetupComplete) return;
+ if (!IsOpen) return;
+ if (_isRefreshing) return;
+
+ try
+ {
+ _isRefreshing = true;
+ InventoryState.RefreshFromGame();
+ RefreshCategoriesCore(autosize: true);
+ }
+ finally
+ {
+ _isRefreshing = false;
+ }
+ }
+
+ protected virtual void RefreshCategoriesCore(bool autosize)
+ {
+ if (!_isSetupComplete)
+ return;
+
+ var config = System.Config.General;
+ string searchText = SearchInputNode.SearchString.ExtractText();
+ bool isSearching = !string.IsNullOrWhiteSpace(searchText);
+
+ if (config.SearchMode == SearchMode.Highlight && isSearching)
+ {
+ _searchMatchScratch.Clear();
+ var allData = InventoryState.GetCategories(string.Empty);
+
+ for (int i = 0; i < allData.Count; i++)
+ {
+ var cat = allData[i];
+ for (int j = 0; j < cat.Items.Count; j++)
+ {
+ var item = cat.Items[j];
+ if (item.IsRegexMatch(searchText))
+ {
+ _searchMatchScratch.Add(item.Item.ItemId);
+ }
+ }
+ }
+ HighlightState.SetFilter(HighlightSource.Search, _searchMatchScratch);
+ }
+ else
+ {
+ HighlightState.ClearFilter(HighlightSource.Search);
+ }
+
+ if (SearchInputNode != null)
+ {
+ bool atActive = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey);
+
+ SearchInputNode.HintAddColor = (atActive)
+ ? new Vector3(0.0f, 0.3f, 0.3f)
+ : Vector3.Zero;
+ }
+
+ if (HasFooter)
+ {
+ FooterNode.SlotAmountText = InventoryState.GetEmptySlotsString();
+ FooterNode.RefreshCurrencies();
+ }
+
+ string dataFilter = config.SearchMode == SearchMode.Filter ? searchText : string.Empty;
+ var categories = InventoryState.GetCategories(dataFilter);
+
+ float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
+ int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
+
+ CategoriesNode.SyncWithListDataByKey(
+ dataList: categories,
+ getKeyFromData: categorizedInventory => categorizedInventory.Key,
+ getKeyFromNode: node => node.CategorizedInventory.Key,
+ updateNode: (node, data) =>
+ {
+ node.CategorizedInventory = data;
+ node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
+ node.RefreshNodeVisuals();
+ },
+ createNodeMethod: _ => CreateCategoryNode());
+
+ if (HasPinning)
+ {
+ bool pinsChanged = PinCoordinator.ApplyPinnedStates(CategoriesNode);
+ if (pinsChanged) HoverCoordinator.ResetAll(CategoriesNode);
+ }
+
+ WireHoverHandlers();
+
+ if (autosize)
+ AutoSizeWindow();
+ else
+ {
+ LayoutContent();
+ CategoriesNode.RecalculateLayout();
+ }
+ }
+
+ protected readonly struct HeaderLayout
+ {
+ public Vector2 SearchPosition { get; init; }
+ public Vector2 SearchSize { get; init; }
+ public float HeaderWidth { get; init; }
+ public float HeaderY { get; init; }
+ }
+
+ protected HeaderLayout CalculateHeaderLayout(AtkUnitBase* addon)
+ {
+ var header = addon->WindowHeaderCollisionNode;
+ float headerW = header->Width;
+ float headerH = header->Height;
+
+ // Center the search bar, width is 50% of header
+ float searchWidth = headerW * 0.5f;
+ var searchSize = new Vector2(searchWidth, 28f);
+
+ float searchX = (headerW - searchWidth) * 0.5f;
+ float itemY = header->Y + (headerH - 28f) * 0.5f;
+
+ return new HeaderLayout
+ {
+ SearchPosition = new Vector2(searchX, itemY),
+ SearchSize = searchSize,
+ HeaderWidth = headerW,
+ HeaderY = itemY
+ };
+ }
+
+
+ protected void InitializeBackgroundDropTarget()
+ {
+ BackgroundDropTarget = new DragDropNode
+ {
+ Position = ContentStartPosition,
+ Size = ContentSize,
+ IconId = 0,
+ IsDraggable = false,
+ IsClickable = false,
+ AcceptedType = DragDropType.Item,
+ };
+
+ BackgroundDropTarget.DragDropBackgroundNode.IsVisible = false;
+ BackgroundDropTarget.IconNode.IsVisible = false;
+
+ BackgroundDropTarget.OnPayloadAccepted = OnBackgroundPayloadAccepted;
+
+ BackgroundDropTarget.AttachNode(this);
+ }
+
+ protected virtual InventoryCategoryNode CreateCategoryNode()
+ {
+ return new InventoryCategoryNode
+ {
+ Size = ContentSize with { Y = 120 },
+ OnRefreshRequested = ManualRefresh,
+ OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true),
+ };
+ }
+
+ private void OnBackgroundPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload)
+ {
+ if (!acceptedPayload.IsValidInventoryPayload) return;
+
+ InventoryLocation emptyLocation = InventoryScanner.GetFirstEmptySlot(InventoryState.SourceType);
+
+ if (!emptyLocation.IsValid)
+ {
+ Services.Logger.Error("No empty slots available to receive drop.");
+ return;
+ }
+
+ InventoryMappedLocation visualLocation = InventoryContextState.GetVisualLocation(emptyLocation.Container, emptyLocation.Slot);
+
+ var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
+ int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
+
+ var targetPayload = new DragDropPayload
+ {
+ Type = DragDropType.Item,
+ Int1 = visualLocation.Container,
+ Int2 = visualLocation.Slot,
+ ReferenceIndex = (short)absoluteIndex
+ };
+
+ Services.Logger.DebugOnly($"[BackgroundDrop] Target: {emptyLocation} -> Visual: {visualLocation} (Ref: {absoluteIndex})");
+
+ InventoryMoveHelper.HandleItemMovePayload(acceptedPayload, targetPayload);
+
+ ManualRefresh();
+ }
+
+ protected void WireHoverHandlers()
+ {
+ var nodes = CategoriesNode.Nodes;
+
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ if (nodes[i] is not InventoryCategoryNode node)
+ continue;
+
+ if (!HoverSubscribed.Add(node))
+ continue;
+
+ node.HeaderHoverChanged += (src, hovering) =>
+ {
+ HoverCoordinator.OnCategoryHoverChanged(CategoriesNode, src, hovering);
+ };
+ }
+ }
+
+ protected int CalculateOptimalItemsPerLine(float availableWidth)
+ => Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
+
+ protected virtual void LayoutContent()
+ {
+ Vector2 contentPos = ContentStartPosition;
+ Vector2 contentSize = ContentSize;
+
+ float footerH = HasFooter || HasSlotCounter ? FooterHeight : 0;
+
+ if (HasFooter)
+ {
+ FooterNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH);
+ FooterNode.Size = new Vector2(contentSize.X, footerH);
+ }
+ else if (HasSlotCounter && SlotCounterNode != null)
+ {
+ SlotCounterNode.Position = new Vector2(contentSize.X -80f, contentPos.Y + contentSize.Y - footerH + 4f);
+ }
+
+ float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0);
+ if (gridH < 0) gridH = 0;
+
+ CategoriesNode.Position = contentPos;
+ CategoriesNode.Size = new Vector2(contentSize.X, gridH);
+ }
+
+ protected virtual void AutoSizeWindow()
+ {
+ var nodes = CategoriesNode.Nodes;
+
+ float maxChildWidth = 0f;
+ int childCount = 0;
+
+ for (int i = 0; i < nodes.Count; i++)
+ {
+ if (nodes[i] is not InventoryCategoryNode cat)
+ continue;
+
+ childCount++;
+ float w = cat.Width;
+ if (w > maxChildWidth) maxChildWidth = w;
+ }
+
+ if (childCount == 0)
+ {
+ ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
+ return;
+ }
+
+ float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
+ float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
+
+ float contentWidth = finalWidth - (ContentStartPosition.X * 2);
+
+ float footerSpace = HasFooter || HasSlotCounter ? FooterHeight + FooterTopSpacing : 0;
+ float gridBudget = Math.Max(0f, MaxWindowHeight - footerSpace);
+
+ CategoriesNode.Position = ContentStartPosition;
+ CategoriesNode.Size = new Vector2(contentWidth, gridBudget);
+
+ CategoriesNode.RecalculateLayout();
+
+ float requiredGridHeight = CategoriesNode.GetRequiredHeight();
+ float requiredContentHeight = requiredGridHeight + footerSpace;
+
+ float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
+ float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
+
+ ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
+ }
+
+ protected void ResizeWindow(float width, float height, bool recalcLayout)
+ {
+ SetWindowSize(width, height);
+
+ if (BackgroundDropTarget != null)
+ {
+ BackgroundDropTarget.Size = ContentSize;
+ }
+
+ LayoutContent();
+
+ if (recalcLayout)
+ CategoriesNode.RecalculateLayout();
+ }
+
+ protected void ResizeWindow(float width, float height)
+ => ResizeWindow(width, height, recalcLayout: true);
+
+ public void ItemRefresh() => RefreshCategoriesCore(false);
+
+ protected override void OnRequestedUpdate(AtkUnitBase* addon, NumberArrayData** numberArrayData, StringArrayData** stringArrayData)
+ {
+ base.OnRequestedUpdate(addon, numberArrayData, stringArrayData);
+
+ InventoryState.RefreshFromGame();
+ RefreshCategoriesCore(autosize: true);
+ }
+
+ protected override void OnSetup(AtkUnitBase* addon)
+ {
+ ContextMenu = new ContextMenu();
+
+ base.OnSetup(addon);
+ }
+
+ protected override void OnUpdate(AtkUnitBase* addon)
+ {
+ if (RefreshQueued)
+ {
+ bool doAutosize = RefreshAutosizeQueued;
+ RefreshQueued = false;
+ RefreshAutosizeQueued = false;
+
+ RefreshCategoriesCore(doAutosize);
+ }
+
+ base.OnUpdate(addon);
+ }
+
+ protected override void OnFinalize(AtkUnitBase* addon)
+ {
+ ContextMenu?.Dispose();
+ HoverSubscribed.Clear();
+ RefreshQueued = false;
+ RefreshAutosizeQueued = false;
+
+ base.OnFinalize(addon);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Addons/InventoryAddonContextMenu.cs b/AetherBags/Addons/InventoryAddonContextMenu.cs
new file mode 100644
index 0000000..cee746a
--- /dev/null
+++ b/AetherBags/Addons/InventoryAddonContextMenu.cs
@@ -0,0 +1,84 @@
+using System;
+using AetherBags.Configuration;
+using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using KamiToolKit.Classes.ContextMenu;
+
+namespace AetherBags.Addons;
+
+public static class InventoryAddonContextMenu
+{
+ private static ContextMenuItem Separator => new()
+ {
+ Name = "---------------------------",
+ IsEnabled = false,
+ OnClick = () => { }
+ };
+
+ public static void OpenMain(InventoryAddonBase parent)
+ {
+ if (parent?.ContextMenu == null || System.Config == null) return;
+
+ var menu = parent.ContextMenu;
+ menu.Clear();
+
+ bool hasActiveAtFilter = !string.IsNullOrEmpty(HighlightState.SelectedAllaganToolsFilterKey);
+ string searchText = parent.GetSearchText();
+ if (HighlightState.IsFilterActive || hasActiveAtFilter || !string.IsNullOrEmpty(searchText))
+ {
+ menu.AddItem("Clear All Filters", () =>
+ {
+ HighlightState.ClearAll();
+ parent.SetSearchText(string.Empty);
+ InventoryOrchestrator.RefreshAll(updateMaps: false);
+ });
+ menu.AddItem(Separator);
+ }
+
+ var currentMode = System.Config.General.SearchMode;
+ string modeLabel = currentMode == SearchMode.Filter ? "Mode: Hide Non-Matches" : "Mode: Fade Non-Matches";
+ menu.AddItem(modeLabel, () =>
+ {
+ System.Config.General.SearchMode = currentMode == SearchMode.Filter ? SearchMode.Highlight : SearchMode.Filter;
+ parent.ManualRefresh();
+ });
+
+ if (System.IPC.AllaganTools is { IsReady: true } && System.Config.Categories.AllaganToolsCategoriesEnabled)
+ {
+ var atFilters = System.IPC.AllaganTools.GetSearchFilters();
+ if (atFilters is { Count: > 0 })
+ {
+ var subMenu = new ContextMenuSubItem
+ {
+ Name = "Allagan Tools Filters...",
+ OnClick = () => { }
+ };
+
+ foreach (var (key, name) in atFilters)
+ {
+ var capturedKey = key;
+ bool isActive = HighlightState.SelectedAllaganToolsFilterKey == key;
+ subMenu.AddItem(isActive ?$"✓ {name}" : $" {name}", () =>
+ {
+ HighlightState.SelectedAllaganToolsFilterKey = isActive ? string.Empty : capturedKey;
+ InventoryOrchestrator.RefreshAll(updateMaps: false);
+ });
+ }
+
+ menu.AddItem(subMenu);
+ }
+ }
+
+ menu.Open();
+ }
+
+ public static unsafe void Close()
+ {
+ var agent = AgentContext.Instance();
+ if (agent != null)
+ {
+ agent->ClearMenu();
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Commands/CommandHandler.cs b/AetherBags/Commands/CommandHandler.cs
index 3d36d4e..275f84d 100644
--- a/AetherBags/Commands/CommandHandler.cs
+++ b/AetherBags/Commands/CommandHandler.cs
@@ -1,7 +1,9 @@
using System;
using AetherBags.Helpers;
using AetherBags.Inventory;
+using AetherBags.Inventory.State;
using Dalamud.Game.Command;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace AetherBags.Commands;
@@ -28,7 +30,7 @@ public class CommandHandler : IDisposable
});
}
- private void OnCommand(string command, string args)
+ private unsafe void OnCommand(string command, string args)
{
var argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty;
@@ -57,7 +59,7 @@ public class CommandHandler : IDisposable
break;
case "refresh":
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
PrintChat("Inventory refreshed.");
break;
@@ -67,7 +69,7 @@ public class CommandHandler : IDisposable
case "import-sk":
ImportExportResetHelper.TryImportSortaKindaFromClipboard(true);
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
break;
case "export":
@@ -76,12 +78,12 @@ public class CommandHandler : IDisposable
case "import":
ImportExportResetHelper.TryImportConfigFromClipboard();
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
break;
case "reset":
ImportExportResetHelper.TryResetConfig();
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
break;
case "count":
@@ -90,6 +92,14 @@ public class CommandHandler : IDisposable
PrintChat($"{stats.UsedSlots}/{stats.TotalSlots} slots used ({stats.UsagePercent:F0}%) | {stats.TotalItems} unique items | {stats.CategoryCount} categories");
break;
+ case "saddle":
+ System.AddonSaddleBagWindow.Toggle();
+ break;
+
+ case "retainer":
+ System.AddonRetainerWindow.Toggle();
+ break;
+
case "help":
case "?":
PrintHelp();
diff --git a/AetherBags/Configuration/CategorySettings.cs b/AetherBags/Configuration/CategorySettings.cs
index 898a201..12547d3 100644
--- a/AetherBags/Configuration/CategorySettings.cs
+++ b/AetherBags/Configuration/CategorySettings.cs
@@ -8,8 +8,12 @@ namespace AetherBags.Configuration;
public class CategorySettings
{
+ public bool CategoriesEnabled { get; set; } = true;
public bool GameCategoriesEnabled { get; set; } = true;
public bool UserCategoriesEnabled { get; set; } = true;
+ public bool BisBuddyEnabled { get; set; } = true;
+ public bool AllaganToolsCategoriesEnabled { get; set; } = false;
+ public AllaganToolsFilterMode AllaganToolsMode { get; set; } = AllaganToolsFilterMode.Highlight;
public List UserCategories { get; set; } = new();
}
@@ -75,4 +79,10 @@ public enum ToggleFilterState
Ignored = 0,
Allow = 1,
Disallow = 2,
+}
+
+public enum AllaganToolsFilterMode
+{
+ Categorize = 0,
+ Highlight = 1,
}
\ No newline at end of file
diff --git a/AetherBags/Configuration/GeneralSettings.cs b/AetherBags/Configuration/GeneralSettings.cs
index c8e8b3c..79583f5 100644
--- a/AetherBags/Configuration/GeneralSettings.cs
+++ b/AetherBags/Configuration/GeneralSettings.cs
@@ -3,6 +3,7 @@ namespace AetherBags.Configuration;
public class GeneralSettings
{
public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId;
+ public SearchMode SearchMode { get; set; } = SearchMode.Highlight;
public bool DebugEnabled { get; set; } = false;
public bool CompactPackingEnabled { get; set; } = true;
public int CompactLookahead { get; set; } = 24;
@@ -10,6 +11,10 @@ public class GeneralSettings
public bool CompactStableInsert { get; set; } = true;
public bool OpenWithGameInventory { get; set; } = true;
public bool HideGameInventory { get; set; } = false;
+ public bool OpenSaddleBagsWithGameInventory { get; set; } = true;
+ public bool HideGameSaddleBags { get; set; } = false;
+ public bool OpenRetainerWithGameInventory { get; set; } = true;
+ public bool HideGameRetainer { get; set; } = false;
public bool ShowCategoryItemCount { get; set; } = false;
public bool LinkItemEnabled { get; set; } = false;
}
@@ -18,4 +23,10 @@ public enum InventoryStackMode : byte
{
NaturalStacks = 0,
AggregateByItemId = 1,
+}
+
+public enum SearchMode : byte
+{
+ Filter = 0,
+ Highlight = 1,
}
\ No newline at end of file
diff --git a/AetherBags/Extensions/AgentInterfaceExtensions.cs b/AetherBags/Extensions/AgentInterfaceExtensions.cs
new file mode 100644
index 0000000..2236e0a
--- /dev/null
+++ b/AetherBags/Extensions/AgentInterfaceExtensions.cs
@@ -0,0 +1,23 @@
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+
+namespace AetherBags.Extensions;
+
+public static unsafe class AgentInterfaceExtensions {
+
+ extension(ref AgentInterface agent)
+ {
+ public void SendCommand(uint eventKind, int[] commandValues)
+ {
+ using var returnValue = new AtkValue();
+ var command = stackalloc AtkValue[commandValues.Length];
+
+ for (var index = 0; index < commandValues.Length; index++)
+ {
+ command[index].SetInt(commandValues[index]);
+ }
+
+ agent.ReceiveEvent(&returnValue, command, (uint)commandValues.Length, eventKind);
+ }
+ }
+}
diff --git a/AetherBags/Extensions/AtkResNodeExtensions.cs b/AetherBags/Extensions/AtkResNodeExtensions.cs
deleted file mode 100644
index f5da790..0000000
--- a/AetherBags/Extensions/AtkResNodeExtensions.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-
-namespace AetherBags.Extensions;
-
-public static unsafe class AtkResNodeExtensions
-{
- extension(ref AtkResNode node)
- {
- public void ShowInventoryItemTooltip(InventoryType container, short slot) {
- fixed (AtkResNode* nodePointer = &node) {
- AtkStage.Instance()->ShowInventoryItemTooltip(nodePointer, container, slot);
- }
- }
- }
-}
\ No newline at end of file
diff --git a/AetherBags/Extensions/DragDropPayloadExtensions.cs b/AetherBags/Extensions/DragDropPayloadExtensions.cs
index a8cd0dd..197f6ed 100644
--- a/AetherBags/Extensions/DragDropPayloadExtensions.cs
+++ b/AetherBags/Extensions/DragDropPayloadExtensions.cs
@@ -1,4 +1,4 @@
-using AetherBags.Interop;
+
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -8,55 +8,8 @@ using Lumina.Text;
namespace AetherBags.Extensions;
-// TODO: Remove FixedInterface when CS is merged into Dalamud.
public static unsafe class DragDropPayloadExtensions
{
- public static DragDropPayload FromFixedInterface(AtkDragDropInterface* dragDropInterface)
- {
- // Cast to our manual fixed struct
- var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface;
-
- // Calls Index 12
- var payloadContainer = fixedInterface->GetPayloadContainer();
-
- return new DragDropPayload
- {
- Type = fixedInterface->DragDropType,
- ReferenceIndex = fixedInterface->DragDropReferenceIndex,
- Int1 = payloadContainer->Int1,
- Int2 = payloadContainer->Int2,
- Text = new ReadOnlySeString(payloadContainer->Text),
- };
- }
-
- public static void ToFixedInterface(this DragDropPayload payload, AtkDragDropInterface* dragDropInterface, bool writeToPayloadContainer = true)
- {
- var fixedInterface = (AtkDragDropInterfaceFixed*)dragDropInterface;
-
- fixedInterface->DragDropType = payload.Type;
- fixedInterface->DragDropReferenceIndex = payload.ReferenceIndex;
-
- if (writeToPayloadContainer)
- {
- // Calls Index 12
- var payloadContainer = fixedInterface->GetPayloadContainer();
-
- payloadContainer->Clear();
- payloadContainer->Int1 = payload.Int1;
- payloadContainer->Int2 = payload.Int2;
-
- if (payload.Text.IsEmpty)
- {
- payloadContainer->Text.Clear();
- }
- else
- {
- var stringBuilder = new SeStringBuilder().Append(payload.Text);
- payloadContainer->Text.SetString(stringBuilder.GetViewAsSpan());
- }
- }
- }
-
extension(DragDropPayload payload)
{
public bool IsValidInventoryPayload =>
@@ -65,6 +18,15 @@ public static unsafe class DragDropPayloadExtensions
or DragDropType.RemoteInventory_Item
or DragDropType.Item;
+ public bool IsSameBaseContainer(DragDropPayload otherPayload) {
+ if (payload.InventoryLocation.Container.IsSameContainerGroup(otherPayload.InventoryLocation.Container))
+ {
+ return true;
+ }
+
+ return false;
+ }
+
public InventoryLocation InventoryLocation
{
get
@@ -84,7 +46,7 @@ public static unsafe class DragDropPayloadExtensions
if (sourceContainer == 0)
return new InventoryLocation(0, 0);
- // Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots
+ // Retainers have special handling: UI has 5 tabs × 35 slots, data has 7 pages × 25 slots
if (sourceContainer.IsRetainer)
{
// Container IDs 52-56 = UI tabs 0-4
diff --git a/AetherBags/Extensions/InventoryItemExtensions.cs b/AetherBags/Extensions/InventoryItemExtensions.cs
index 7189894..1dbe710 100644
--- a/AetherBags/Extensions/InventoryItemExtensions.cs
+++ b/AetherBags/Extensions/InventoryItemExtensions.cs
@@ -115,6 +115,9 @@ public static unsafe class InventoryItemExtensions {
if (InventoryManager.Instance()->GetInventoryItemCount(itemId, true) > 0)
itemId += 1_000_000;
+ if (!item.Container.IsMainInventory)
+ return;
+
AgentInventoryContext.Instance()->UseItem(itemId, type);
}
}
diff --git a/AetherBags/Extensions/InventoryTypeExtensions.cs b/AetherBags/Extensions/InventoryTypeExtensions.cs
index 37a2b7d..77c6e43 100644
--- a/AetherBags/Extensions/InventoryTypeExtensions.cs
+++ b/AetherBags/Extensions/InventoryTypeExtensions.cs
@@ -1,6 +1,7 @@
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+using InventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager;
namespace AetherBags.Extensions;
@@ -107,17 +108,17 @@ public static unsafe class InventoryTypeExtensions
};
public int GetInventoryStartIndex => inventoryType switch {
- InventoryType.Inventory2 => inventoryType.GetInventorySorter->ItemsPerPage,
- InventoryType.Inventory3 => inventoryType.GetInventorySorter->ItemsPerPage * 2,
- InventoryType.Inventory4 => inventoryType.GetInventorySorter->ItemsPerPage * 3,
- InventoryType.SaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage,
- InventoryType.PremiumSaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage,
- InventoryType.RetainerPage2 => inventoryType.GetInventorySorter->ItemsPerPage,
- InventoryType.RetainerPage3 => inventoryType.GetInventorySorter->ItemsPerPage * 2,
- InventoryType.RetainerPage4 => inventoryType.GetInventorySorter->ItemsPerPage * 3,
- InventoryType.RetainerPage5 => inventoryType.GetInventorySorter->ItemsPerPage * 4,
- InventoryType.RetainerPage6 => inventoryType.GetInventorySorter->ItemsPerPage * 5,
- InventoryType.RetainerPage7 => inventoryType.GetInventorySorter->ItemsPerPage * 6,
+ InventoryType.Inventory2 => inventoryType.UIPageSize,
+ InventoryType.Inventory3 => inventoryType.UIPageSize * 2,
+ InventoryType.Inventory4 => inventoryType.UIPageSize * 3,
+ InventoryType.SaddleBag2 => inventoryType.UIPageSize,
+ InventoryType.PremiumSaddleBag2 => inventoryType.UIPageSize,
+ InventoryType.RetainerPage2 => inventoryType.UIPageSize,
+ InventoryType.RetainerPage3 => inventoryType.UIPageSize * 2,
+ InventoryType.RetainerPage4 => inventoryType.UIPageSize * 3,
+ InventoryType.RetainerPage5 => inventoryType.UIPageSize * 4,
+ InventoryType.RetainerPage6 => inventoryType.UIPageSize * 5,
+ InventoryType.RetainerPage7 => inventoryType.UIPageSize * 6,
_ => 0,
};
@@ -156,6 +157,14 @@ public static unsafe class InventoryTypeExtensions
InventoryType.RetainerPage6 or
InventoryType.RetainerPage7;
+ public int UIPageSize => inventoryType switch
+ {
+ _ when (inventoryType.IsMainInventory || inventoryType.IsRetainer) => 35,
+ _ when inventoryType.IsSaddleBag => 70,
+ _ when inventoryType.IsArmory => 50,
+ _ => 0,
+ };
+
public int ContainerGroup => inventoryType switch
{
_ when inventoryType.IsMainInventory => 1,
@@ -165,6 +174,8 @@ public static unsafe class InventoryTypeExtensions
_ => 0,
};
+ public bool IsLoaded => InventoryManager.Instance()->GetInventoryContainer(inventoryType)->IsLoaded;
+
public bool IsSameContainerGroup(InventoryType other)
=> inventoryType.ContainerGroup == other.ContainerGroup;
diff --git a/AetherBags/Extensions/LoggerExtensions.cs b/AetherBags/Extensions/LoggerExtensions.cs
index dd6e759..3882e87 100644
--- a/AetherBags/Extensions/LoggerExtensions.cs
+++ b/AetherBags/Extensions/LoggerExtensions.cs
@@ -2,13 +2,16 @@ namespace AetherBags.Extensions;
public static class LoggerExtensions
{
- public static void DebugOnly(this object logger, string message)
+ extension(object logger)
{
- if(System.Config.General.DebugEnabled) Services.Logger.Debug(message);
- }
+ public void DebugOnly(string message)
+ {
+ if (System.Config?.General?.DebugEnabled == true)
+ {
+ Services.Logger.DebugOnly(message);
+ }
+ }
- public static void DebugOnly(this object logger, string message, params object[] args)
- {
- if(System.Config.General.DebugEnabled) Services.Logger.Debug(message);
+ public void DebugOnly(string message, params object[] args) => DebugOnly(logger, string.Format(message, args));
}
}
\ No newline at end of file
diff --git a/AetherBags/Helpers/BackupHelper.cs b/AetherBags/Helpers/BackupHelper.cs
index 60466cc..4446e13 100644
--- a/AetherBags/Helpers/BackupHelper.cs
+++ b/AetherBags/Helpers/BackupHelper.cs
@@ -14,7 +14,7 @@ public static class BackupHelper {
private const int MaxBackups = 10;
private const string Name = "AetherBags";
public static void DoConfigBackup(IDalamudPluginInterface pluginInterface) {
- Services.Logger.Debug("Backup configuration start.");
+ Services.Logger.DebugOnly("Backup configuration start.");
try {
var configDirectory = pluginInterface.ConfigDirectory;
if (!configDirectory.Exists) {
diff --git a/AetherBags/Helpers/InventoryMoveHelper.cs b/AetherBags/Helpers/InventoryMoveHelper.cs
index 9fef5cc..4523303 100644
--- a/AetherBags/Helpers/InventoryMoveHelper.cs
+++ b/AetherBags/Helpers/InventoryMoveHelper.cs
@@ -1,45 +1,52 @@
+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;
namespace AetherBags. Helpers;
public static unsafe class InventoryMoveHelper
{
- // Requires the visual UI slots instead of actual slots.
public static void MoveItem(InventoryType sourceContainer, ushort sourceSlot, InventoryType destContainer, ushort destSlot)
{
- Services.Logger.Debug($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}");
+ Services.Logger.DebugOnly($"[MoveItem] {sourceContainer}@{sourceSlot} -> {destContainer}@{destSlot}");
InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
- Services.Framework.DelayTicks(2);
- Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualInventoryRefresh);
+ Services.Framework.DelayTicks(3);
+ Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualRefresh);
}
- /*
- private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot)
+ public static void HandleItemMovePayload(DragDropPayload source, DragDropPayload target)
{
- uint sourceContainerId = sourceInventory.AgentItemContainerId;
- uint destContainerId = destInventory.AgentItemContainerId;
+ uint srcContainer = (uint)source.Int1;
+ uint dstContainer = (uint)target.Int1;
- if (sourceContainerId == 0 || destContainerId == 0)
- {
- Services.Logger.Warning($"[MoveItemViaAgent] Invalid container IDs: src={sourceContainerId}, dst={destContainerId}");
- return;
- }
+ uint srcSlot = (uint)source.Int2;
+ uint dstSlot = (uint)target.Int2;
- Services.Logger.Debug($"[MoveItemViaAgent] {sourceContainerId}:{sourceSlot} -> {destContainerId}:{destSlot}");
+ short srcRi = source.ReferenceIndex;
+ short dstRi = target.ReferenceIndex;
+
+ if (srcContainer == 0 || dstContainer == 0) return;
+
+ Services.Logger.DebugOnly($"[MoveItemViaAgent] {srcContainer}:{srcSlot}:{srcRi} -> {dstContainer}:{dstSlot}:{dstRi}");
var atkValues = stackalloc AtkValue[4];
for (var i = 0; i < 4; i++)
- atkValues[i]. Type = ValueType.UInt;
+ {
+ atkValues[i].Type = ValueType.UInt;
+ }
- atkValues[0].SetUInt(sourceContainerId);
- atkValues[1].SetUInt(sourceSlot);
- atkValues[2].SetUInt(destContainerId);
- atkValues[3].SetUInt(destSlot);
+ atkValues[0].UInt = srcContainer;
+ atkValues[1].UInt = srcSlot;
+ atkValues[2].UInt = dstContainer;
+ atkValues[3].UInt = dstSlot;
var retVal = stackalloc AtkValue[1];
RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
atkModule->HandleItemMove(retVal, atkValues, 4);
}
- */
}
\ No newline at end of file
diff --git a/AetherBags/Hooks/InventoryHook.cs b/AetherBags/Hooks/InventoryHook.cs
index f5df87c..996b84d 100644
--- a/AetherBags/Hooks/InventoryHook.cs
+++ b/AetherBags/Hooks/InventoryHook.cs
@@ -37,7 +37,7 @@ public sealed unsafe class InventoryHooks : IDisposable
MoveItemSlotDetour);
_moveItemSlotHook.Enable();
- Services.Logger.Debug("MoveItemSlot hooked successfully.");
+ Services.Logger.DebugOnly("MoveItemSlot hooked successfully.");
}
catch (Exception e)
{
@@ -51,7 +51,7 @@ public sealed unsafe class InventoryHooks : IDisposable
OpenInventoryDetour);
_openInventoryHook.Enable();
- Services.Logger.Debug("OpenInventory hooked successfully.");
+ Services.Logger.DebugOnly("OpenInventory hooked successfully.");
}
catch (Exception e)
{
@@ -64,7 +64,7 @@ public sealed unsafe class InventoryHooks : IDisposable
HandleInventoryEventDetour);
_handleInventoryEventHook.Enable();
- Services.Logger.Debug("HandleInventoryEvent hooked successfully.");
+ Services.Logger.DebugOnly("HandleInventoryEvent hooked successfully.");
}
catch (Exception e)
{
@@ -77,7 +77,7 @@ public sealed unsafe class InventoryHooks : IDisposable
OpenAddonDetour);
_openAddonHook.Enable();
- Services.Logger.Debug("OpenAddon hooked successfully.");
+ Services.Logger.DebugOnly("OpenAddon hooked successfully.");
}
catch (Exception e)
{
@@ -93,10 +93,11 @@ public sealed unsafe class InventoryHooks : IDisposable
ushort dstSlot,
bool unk)
{
- InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
- InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
+ //InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
+ //InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot);
- Services.Logger.Debug($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
+ Services.Logger.DebugOnly($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} -> {dstType}@{dstSlot} I Unk: {unk}");
+ //Services.Logger.DebugOnly($"[MoveItemSlot Hook] Moving {srcType}@{srcSlot} ID:{sourceItem->ItemId} -> {dstType}@{dstSlot} ID:{destItem->ItemId} Unk: {unk}");
return _moveItemSlotHook!.Original(manager, srcType, srcSlot, dstType, dstSlot, unk);
}
@@ -104,7 +105,7 @@ public sealed unsafe class InventoryHooks : IDisposable
/*
private void OpenInventoryDetour(UIModule* uiModule, byte type)
{
- Services.Logger.Debug($"[OpenInventory Hook] Opening inventory of type {type}");
+ Services.Logger.DebugOnly($"[OpenInventory Hook] Opening inventory of type {type}");
_openInventoryHook?.Original(uiModule, type);
}
@@ -112,7 +113,7 @@ public sealed unsafe class InventoryHooks : IDisposable
{
for(int i = 0; i < valueCount; i++)
{
- Services.Logger.Debug($"[HandleInventoryEvent Hook] AtkValue[{i}]: Type={atkValue[i].Type}, ToString: {atkValue[i].ToString()} ");
+ Services.Logger.DebugOnly($"[HandleInventoryEvent Hook] AtkValue[{i}]: Type={atkValue[i].Type}, ToString: {atkValue[i].ToString()} ");
}
_handleInventoryEventHook?.Original(eventInterface, atkValue, valueCount);
}
@@ -121,7 +122,7 @@ public sealed unsafe class InventoryHooks : IDisposable
{
for(int i = 0; i < valueCount; i++)
{
- Services.Logger.Debug($"[OpenAddon Hook] AtkValue[{i}]: ToString: {values[i].ToString()} ");
+ Services.Logger.DebugOnly($"[OpenAddon Hook] AtkValue[{i}]: ToString: {values[i].ToString()} ");
}
return _openAddonHook!.Original(thisPtr, addonNameId, valueCount, values, eventInterface, eventKind, parentAddonId, depthLayer);
}
diff --git a/AetherBags/IPC/AllaganToolsIPC.cs b/AetherBags/IPC/AllaganToolsIPC.cs
new file mode 100644
index 0000000..c534b99
--- /dev/null
+++ b/AetherBags/IPC/AllaganToolsIPC.cs
@@ -0,0 +1,195 @@
+using System;
+using System.Collections.Generic;
+using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
+using Dalamud.Plugin.Ipc;
+
+namespace AetherBags.IPC;
+
+public class AllaganToolsIPC : IDisposable
+{
+ private ICallGateSubscriber? _isInitialized;
+ private ICallGateSubscriber? _initialized;
+ private ICallGateSubscriber>? _getFilterItems;
+ private ICallGateSubscriber>? _getSearchFilters;
+ private ICallGateSubscriber? _enableUiFilter;
+ private ICallGateSubscriber? _toggleUiFilter;
+
+ public bool IsReady { get; private set; }
+
+ ///
+ /// Cached filter items. Key = filterKey, Value = (ItemId -> Quantity).
+ ///
+ public Dictionary> CachedFilterItems { get; } = new();
+
+ ///
+ /// Cached search filters. Key -> Name.
+ ///
+ public Dictionary CachedSearchFilters { get; } = new();
+
+ ///
+ /// Quick lookup: ItemId -> List of filter keys that contain this item.
+ ///
+ public Dictionary> ItemToFilters { get; } = new();
+
+ public event Action? OnInitialized;
+ public event Action? OnFiltersRefreshed;
+
+ public AllaganToolsIPC()
+ {
+ try
+ {
+ _isInitialized = Services.PluginInterface.GetIpcSubscriber("AllaganTools.IsInitialized");
+ _initialized = Services.PluginInterface.GetIpcSubscriber("AllaganTools.Initialized");
+ _getFilterItems = Services.PluginInterface.GetIpcSubscriber>("AllaganTools.GetFilterItems");
+ _getSearchFilters = Services.PluginInterface.GetIpcSubscriber>("AllaganTools.GetSearchFilters");
+ _enableUiFilter = Services.PluginInterface.GetIpcSubscriber("AllaganTools.EnableUiFilter");
+ _toggleUiFilter = Services.PluginInterface.GetIpcSubscriber("AllaganTools.ToggleUiFilter");
+
+ _initialized.Subscribe(OnAllaganInitialized);
+
+ try
+ {
+ IsReady = _isInitialized.InvokeFunc();
+ if (IsReady)
+ {
+ RefreshFilters();
+ }
+ }
+ catch
+ {
+ IsReady = false;
+ }
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.DebugOnly($"Allagan Tools not available: {ex.Message}");
+ IsReady = false;
+ }
+ }
+
+ private void OnAllaganInitialized(bool initialized)
+ {
+ IsReady = initialized;
+ if (initialized)
+ {
+ Services.Logger.Information("Allagan Tools IPC connected");
+ RefreshFilters();
+ OnInitialized?.Invoke();
+ }
+ }
+
+ ///
+ /// Refreshes all cached filter data from Allagan Tools.
+ /// Call this when you need updated filter information.
+ ///
+ public void RefreshFilters()
+ {
+ if (!IsReady) return;
+
+ try
+ {
+ CachedSearchFilters.Clear();
+ CachedFilterItems.Clear();
+ ItemToFilters.Clear();
+
+ var filters = _getSearchFilters?.InvokeFunc();
+ if (filters == null) return;
+
+ foreach (var (key, name) in filters)
+ {
+ CachedSearchFilters[key] = name;
+
+ var items = _getFilterItems?.InvokeFunc(key);
+ if (items != null && items.Count > 0)
+ {
+ CachedFilterItems[key] = items;
+
+ // Build reverse lookup
+ foreach (var itemId in items.Keys)
+ {
+ if (!ItemToFilters.TryGetValue(itemId, out var filterList))
+ {
+ filterList = new List(capacity: 4);
+ ItemToFilters[itemId] = filterList;
+ }
+ filterList.Add(key);
+ }
+ }
+ }
+
+ Services.Logger.DebugOnly($"Refreshed {CachedSearchFilters.Count} Allagan Tools filters, {ItemToFilters.Count} unique items");
+ OnFiltersRefreshed?.Invoke();
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.Warning($"Failed to refresh Allagan Tools filters: {ex.Message}");
+ }
+ }
+
+ ///
+ /// Checks if an item is in any Allagan Tools filter.
+ ///
+ public bool IsItemInAnyFilter(uint itemId)
+ => ItemToFilters.ContainsKey(itemId);
+
+ ///
+ /// Gets all filter keys that contain this item.
+ ///
+ public IReadOnlyList? GetFiltersForItem(uint itemId)
+ => ItemToFilters.TryGetValue(itemId, out var list) ? list : null;
+
+ ///
+ /// Gets items from a specific filter. Returns ItemId -> Quantity.
+ ///
+ public Dictionary? GetFilterItems(string filterKey)
+ {
+ // Try cache first
+ if (CachedFilterItems.TryGetValue(filterKey, out var cached))
+ return cached;
+
+ if (!IsReady) return null;
+
+ try
+ {
+ return _getFilterItems?.InvokeFunc(filterKey);
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.Warning($"GetFilterItems failed: {ex.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Gets all available search filters. Returns Key -> Name.
+ ///
+ public Dictionary? GetSearchFilters()
+ {
+ if (CachedSearchFilters.Count > 0)
+ return CachedSearchFilters;
+
+ if (!IsReady) return null;
+
+ try
+ {
+ return _getSearchFilters?.InvokeFunc();
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.Warning($"GetSearchFilters failed: {ex.Message}");
+ return null;
+ }
+ }
+
+ public void SelectFilter(string filterKey)
+ {
+ HighlightState.SelectedAllaganToolsFilterKey = filterKey;
+ InventoryOrchestrator.RefreshHighlights();
+ }
+
+ public void Dispose()
+ {
+ _initialized?.Unsubscribe(OnAllaganInitialized);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/IPC/BisBuddyIPC.cs b/AetherBags/IPC/BisBuddyIPC.cs
new file mode 100644
index 0000000..1317fe4
--- /dev/null
+++ b/AetherBags/IPC/BisBuddyIPC.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
+using Dalamud.Plugin.Ipc;
+
+namespace AetherBags.IPC;
+
+public class BisBuddyIPC : IDisposable
+{
+ private ICallGateSubscriber? _isInitialized;
+ private ICallGateSubscriber? _initialized;
+ private ICallGateSubscriber>? _getBisItems;
+ private ICallGateSubscriber, bool>? _bisItemsChanged;
+
+ public bool IsReady { get; private set; }
+ private static readonly Vector3 BisColor = new(0.0f, 0.3f, 0.0f);
+
+ public BisBuddyIPC()
+ {
+ try
+ {
+ _isInitialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.IsInitialized");
+ _initialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.Initialized");
+ _getBisItems = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetBisItems");
+ _bisItemsChanged = Services.PluginInterface.GetIpcSubscriber, bool>("BisBuddy.BisItemsChanged");
+
+ _initialized.Subscribe(OnInitialized);
+ _bisItemsChanged.Subscribe(UpdateHighlights);
+
+ try { IsReady = _isInitialized.InvokeFunc(); } catch { IsReady = false; }
+
+ if (IsReady) RequestUpdate();
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.DebugOnly($"BisBuddy not available: {ex.Message}");
+ }
+ }
+
+ private void OnInitialized(bool ready)
+ {
+ IsReady = ready;
+ if (ready) RequestUpdate();
+ else HighlightState.ClearLabel(HighlightSource.BiSBuddy);
+ }
+
+ public void RequestUpdate()
+ {
+ if (!IsReady) return;
+ try
+ {
+ var items = _getBisItems?.InvokeFunc();
+ if (items != null) UpdateHighlights(items);
+ }
+ catch { IsReady = false; }
+ }
+
+ private void UpdateHighlights(List? itemIds)
+ {
+ if (!System.Config.Categories.BisBuddyEnabled || itemIds == null || itemIds.Count == 0)
+ {
+ HighlightState.ClearLabel(HighlightSource.BiSBuddy);
+ }
+ else
+ {
+ HighlightState.SetLabel(HighlightSource.BiSBuddy, itemIds, BisColor);
+ }
+
+ InventoryOrchestrator.RefreshHighlights();
+ }
+
+ public void Dispose()
+ {
+ _initialized?.Unsubscribe(OnInitialized);
+ _bisItemsChanged?.Unsubscribe(UpdateHighlights);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/IPC/IPCService.cs b/AetherBags/IPC/IPCService.cs
new file mode 100644
index 0000000..38bc733
--- /dev/null
+++ b/AetherBags/IPC/IPCService.cs
@@ -0,0 +1,19 @@
+using System;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Ipc;
+
+namespace AetherBags.IPC;
+
+public class IPCService : IDisposable
+{
+ public AllaganToolsIPC AllaganTools { get; } = new();
+ public WotsItIPC WotsIt { get; } = new();
+ public BisBuddyIPC BisBuddy { get; } = new();
+
+ public void Dispose()
+ {
+ AllaganTools.Dispose();
+ WotsIt.Dispose();
+ BisBuddy.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/IPC/WotsItIPC.cs b/AetherBags/IPC/WotsItIPC.cs
new file mode 100644
index 0000000..5e13b15
--- /dev/null
+++ b/AetherBags/IPC/WotsItIPC.cs
@@ -0,0 +1,80 @@
+using System;
+using Dalamud.Plugin.Ipc;
+
+namespace AetherBags.IPC;
+
+public class WotsItIPC : IDisposable
+{
+ private ICallGateSubscriber? _registerWithSearch;
+ private ICallGateSubscriber? _invoke;
+ private ICallGateSubscriber? _unregisterAll;
+
+ private string? _searchGuid;
+
+ public WotsItIPC()
+ {
+ try
+ {
+ _registerWithSearch = Services.PluginInterface.GetIpcSubscriber("FA.RegisterWithSearch");
+ _unregisterAll = Services.PluginInterface.GetIpcSubscriber("FA.UnregisterAll");
+ _invoke = Services.PluginInterface.GetIpcSubscriber("FA.Invoke");
+
+ _invoke.Subscribe(OnInvoke);
+
+ Register();
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.DebugOnly($"WotsIt not available: {ex.Message}");
+ }
+ }
+
+ private void Register()
+ {
+ try
+ {
+ UnregisterAll();
+
+ _searchGuid = _registerWithSearch?.InvokeFunc(
+ Services.PluginInterface.InternalName,
+ "AetherBags: Search Inventory",
+ "AetherBags Search",
+ 66472 // Icon ID
+ );
+ }
+ catch (Exception ex)
+ {
+ Services.Logger.DebugOnly($"Failed to register with WotsIt: {ex.Message}");
+ }
+ }
+
+ private void OnInvoke(string guid)
+ {
+ if (guid == _searchGuid)
+ {
+ if (! System.AddonInventoryWindow.IsOpen)
+ {
+ System.AddonInventoryWindow.Open();
+ }
+ }
+ }
+
+ private bool UnregisterAll()
+ {
+ try
+ {
+ _unregisterAll?.InvokeFunc(Services.PluginInterface.InternalName);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
+ public void Dispose()
+ {
+ _invoke?.Unsubscribe(OnInvoke);
+ UnregisterAll();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Interop/AtkDragDropInterfaceFixed.cs b/AetherBags/Interop/AtkDragDropInterfaceFixed.cs
deleted file mode 100644
index 34daf17..0000000
--- a/AetherBags/Interop/AtkDragDropInterfaceFixed.cs
+++ /dev/null
@@ -1,69 +0,0 @@
-using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-
-namespace AetherBags.Interop;
-
-// Size 0x30 (48) matches the original struct
-[StructLayout(LayoutKind.Explicit, Size = 48)]
-public unsafe struct AtkDragDropInterfaceFixed
-{
- // Offset 0 is the Virtual Table Pointer (void**)
- [FieldOffset(0)] public void** VirtualTable;
-
- // Map specific fields needed for Payload logic
- [FieldOffset(36)] public DragDropType DragDropType;
- [FieldOffset(40)] public short DragDropReferenceIndex;
-
- // Helper to get 'this' as a pointer
- private AtkDragDropInterfaceFixed* ThisPtr => (AtkDragDropInterfaceFixed*)Unsafe.AsPointer(ref this);
-
- // [VirtualFunction(1)]
- public void GetScreenPosition(float* screenX, float* screenY)
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[1];
- fnPtr(ThisPtr, screenX, screenY);
- }
-
- // [VirtualFunction(3)]
- public AtkComponentNode* GetComponentNode()
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[3];
- return fnPtr(ThisPtr);
- }
-
- // [VirtualFunction(5)]
- public void SetComponentNode(AtkComponentNode* node)
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[5];
- fnPtr(ThisPtr, node);
- }
-
- // [VirtualFunction(6)]
- public AtkResNode* GetActiveNode()
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[6];
- return fnPtr(ThisPtr);
- }
-
- // [VirtualFunction(8)]
- public AtkComponentBase* GetComponent()
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[8];
- return fnPtr(ThisPtr);
- }
-
- // [VirtualFunction(9)]
- public bool HandleMouseUpEvent(AtkEventData.AtkMouseData* mouseData)
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[9];
- return fnPtr(ThisPtr, mouseData) != 0;
- }
-
- // [VirtualFunction(12)]
- public AtkDragDropPayloadContainer* GetPayloadContainer()
- {
- var fnPtr = (delegate* unmanaged)VirtualTable[12];
- return fnPtr(ThisPtr);
- }
-}
\ No newline at end of file
diff --git a/AetherBags/Inventory/CategorizedInventory.cs b/AetherBags/Inventory/Categories/CategorizedInventory.cs
similarity index 64%
rename from AetherBags/Inventory/CategorizedInventory.cs
rename to AetherBags/Inventory/Categories/CategorizedInventory.cs
index 6f5a511..8f42bff 100644
--- a/AetherBags/Inventory/CategorizedInventory.cs
+++ b/AetherBags/Inventory/Categories/CategorizedInventory.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
+using AetherBags.Inventory.Items;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Categories;
public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List Items);
\ No newline at end of file
diff --git a/AetherBags/Inventory/Categories/CategoryBucket.cs b/AetherBags/Inventory/Categories/CategoryBucket.cs
new file mode 100644
index 0000000..3dda341
--- /dev/null
+++ b/AetherBags/Inventory/Categories/CategoryBucket.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using AetherBags.Inventory.Items;
+
+namespace AetherBags.Inventory.Categories;
+
+public sealed class CategoryBucket
+{
+ public uint Key;
+ public CategoryInfo Category = null!;
+ public List Items = null!;
+ public List FilteredItems = null!;
+ public bool Used;
+}
+
+public sealed class ItemCountDescComparer : IComparer
+{
+ public static readonly ItemCountDescComparer Instance = new();
+
+ public int Compare(ItemInfo? left, ItemInfo? right)
+ {
+ if (ReferenceEquals(left, right)) return 0;
+ if (left is null) return 1;
+ if (right is null) return -1;
+
+ int leftCount = left.ItemCount;
+ int rightCount = right.ItemCount;
+
+ if (leftCount > rightCount) return -1;
+ if (leftCount < rightCount) return 1;
+ return 0;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs
similarity index 74%
rename from AetherBags/Inventory/CategoryBucketManager.cs
rename to AetherBags/Inventory/Categories/CategoryBucketManager.cs
index a565ee3..0ca3e4d 100644
--- a/AetherBags/Inventory/CategoryBucketManager.cs
+++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs
@@ -1,9 +1,11 @@
-using AetherBags.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
+using AetherBags.Configuration;
+using AetherBags.Inventory.Items;
+using KamiToolKit.Classes;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Categories;
public static class CategoryBucketManager
{
@@ -17,6 +19,15 @@ public static class CategoryBucketManager
public static bool IsUserCategoryKey(uint key)
=> (key & UserCategoryKeyFlag) != 0;
+ private const uint AllaganFilterKeyFlag = 0x4000_0000;
+
+ public static uint MakeAllaganFilterKey(int index)
+ => AllaganFilterKeyFlag | (uint)(index & 0x3FFF_FFFF);
+
+ public static bool IsAllaganFilterKey(uint key)
+ => (key & AllaganFilterKeyFlag) != 0 && (key & UserCategoryKeyFlag) == 0;
+
+
///
/// Resets all buckets for a new refresh cycle.
///
@@ -147,6 +158,74 @@ public static class CategoryBucketManager
}
}
+ public static void BucketByAllaganFilters(
+ Dictionary itemInfoByKey,
+ Dictionary bucketsByKey,
+ HashSet claimedKeys,
+ bool allaganCategoriesEnabled)
+ {
+ if (!allaganCategoriesEnabled) return;
+ if (!System.IPC.AllaganTools.IsReady) return;
+
+ var filters = System.IPC.AllaganTools.CachedSearchFilters;
+ var filterItems = System.IPC.AllaganTools.CachedFilterItems;
+
+ int index = 0;
+ foreach (var (filterKey, filterName) in filters)
+ {
+ if (!filterItems. TryGetValue(filterKey, out var itemIds))
+ {
+ index++;
+ continue;
+ }
+
+ uint bucketKey = MakeAllaganFilterKey(index);
+
+ if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
+ {
+ bucket = new CategoryBucket
+ {
+ Key = bucketKey,
+ Category = new CategoryInfo
+ {
+ Name = $"[AT] {filterName}",
+ Description = $"Allagan Tools filter: {filterName}",
+ Color = ColorHelper.GetColor(32),
+ },
+ Items = new List(capacity: 16),
+ FilteredItems = new List(capacity: 16),
+ Used = true,
+ };
+ bucketsByKey. Add(bucketKey, bucket);
+ }
+ else
+ {
+ bucket.Used = true;
+ bucket.Category.Name = $"[AT] {filterName}";
+ }
+
+ foreach (var itemKvp in itemInfoByKey)
+ {
+ ulong itemKey = itemKvp.Key;
+ ItemInfo item = itemKvp.Value;
+
+ if (claimedKeys.Contains(itemKey))
+ continue;
+
+ if (itemIds.ContainsKey(item.Item.ItemId))
+ {
+ bucket.Items.Add(item);
+ claimedKeys.Add(itemKey);
+ }
+ }
+
+ if (bucket.Items. Count == 0)
+ bucket.Used = false;
+
+ index++;
+ }
+ }
+
public static void BucketUnclaimedToMisc(
Dictionary itemInfoByKey,
Dictionary bucketsByKey,
@@ -214,9 +293,13 @@ public static class CategoryBucketManager
sortedCategoryKeys.Sort((left, right) =>
{
- bool leftCategory = IsUserCategoryKey(left);
- bool rightCategory = IsUserCategoryKey(right);
- if (leftCategory != rightCategory) return leftCategory ? -1 : 1;
+ bool leftUser = IsUserCategoryKey(left);
+ bool rightUser = IsUserCategoryKey(right);
+ bool leftAllagan = IsAllaganFilterKey(left);
+ bool rightAllagan = IsAllaganFilterKey(right);
+ if (leftUser != rightUser) return leftUser ? -1 : 1;
+ if (leftAllagan != rightAllagan) return leftAllagan ? -1 : 1;
+
return left.CompareTo(right);
});
}
@@ -275,32 +358,4 @@ public static class CategoryBucketManager
Name = name,
};
}
-}
-
-public sealed class CategoryBucket
-{
- public uint Key;
- public CategoryInfo Category = null!;
- public List Items = null!;
- public List FilteredItems = null!;
- public bool Used;
-}
-
-public sealed class ItemCountDescComparer : IComparer
-{
- public static readonly ItemCountDescComparer Instance = new();
-
- public int Compare(ItemInfo? left, ItemInfo? right)
- {
- if (ReferenceEquals(left, right)) return 0;
- if (left is null) return 1;
- if (right is null) return -1;
-
- int leftCount = left.ItemCount;
- int rightCount = right.ItemCount;
-
- if (leftCount > rightCount) return -1;
- if (leftCount < rightCount) return 1;
- return 0;
- }
}
\ No newline at end of file
diff --git a/AetherBags/Inventory/CategoryInfo.cs b/AetherBags/Inventory/Categories/CategoryInfo.cs
similarity index 87%
rename from AetherBags/Inventory/CategoryInfo.cs
rename to AetherBags/Inventory/Categories/CategoryInfo.cs
index 819d75e..5ac5588 100644
--- a/AetherBags/Inventory/CategoryInfo.cs
+++ b/AetherBags/Inventory/Categories/CategoryInfo.cs
@@ -1,7 +1,7 @@
using System.Numerics;
using KamiToolKit.Classes;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Categories;
public class CategoryInfo
{
diff --git a/AetherBags/Inventory/InventoryFilter.cs b/AetherBags/Inventory/Categories/InventoryFilter.cs
similarity index 96%
rename from AetherBags/Inventory/InventoryFilter.cs
rename to AetherBags/Inventory/Categories/InventoryFilter.cs
index 96b2545..42d91ca 100644
--- a/AetherBags/Inventory/InventoryFilter.cs
+++ b/AetherBags/Inventory/Categories/InventoryFilter.cs
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
+using AetherBags.Inventory.Items;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Categories;
public static class InventoryFilter
{
diff --git a/AetherBags/Inventory/UserCategoryMatcher.cs b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs
similarity index 98%
rename from AetherBags/Inventory/UserCategoryMatcher.cs
rename to AetherBags/Inventory/Categories/UserCategoryMatcher.cs
index 945b291..32edcdb 100644
--- a/AetherBags/Inventory/UserCategoryMatcher.cs
+++ b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs
@@ -1,8 +1,9 @@
-using AetherBags.Configuration;
using System;
using System.Text.RegularExpressions;
+using AetherBags.Configuration;
+using AetherBags.Inventory.Items;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Categories;
internal static class UserCategoryMatcher
{
diff --git a/AetherBags/Inventory/Context/HighlightState.cs b/AetherBags/Inventory/Context/HighlightState.cs
new file mode 100644
index 0000000..e278e5f
--- /dev/null
+++ b/AetherBags/Inventory/Context/HighlightState.cs
@@ -0,0 +1,53 @@
+using System.Collections.Generic;
+using System.Numerics;
+
+namespace AetherBags.Inventory.Context;
+
+public enum HighlightSource
+{
+ Search,
+ AllaganTools,
+ BiSBuddy,
+}
+
+public static class HighlightState
+{
+ private static readonly Dictionary> Filters = new();
+ private static readonly Dictionary ids, Vector3 color)> Labels = new();
+
+ public static string? SelectedAllaganToolsFilterKey { get; set; } = string.Empty;
+
+ public static bool IsFilterActive => Filters.Count > 0;
+
+ public static void SetFilter(HighlightSource source, IEnumerable ids)
+ => Filters[source] = new HashSet(ids);
+
+ public static bool IsInActiveFilters(uint itemId)
+ {
+ if (Filters.Count == 0) return true;
+ foreach (var filter in Filters.Values)
+ if (filter.Contains(itemId)) return true;
+ return false;
+ }
+
+ public static Vector3? GetLabelColor(uint itemId)
+ {
+ foreach (var label in Labels.Values)
+ if (label.ids.Contains(itemId)) return label.color;
+ return null;
+ }
+
+ public static void SetLabel(HighlightSource source, IEnumerable ids, Vector3 color)
+ => Labels[source] = (new HashSet(ids), color);
+
+
+ public static void ClearAll()
+ {
+ Filters.Clear();
+ Labels.Clear();
+ SelectedAllaganToolsFilterKey = string.Empty;
+ }
+
+ public static void ClearFilter(HighlightSource source) => Filters.Remove(source);
+ public static void ClearLabel(HighlightSource source) => Labels.Remove(source);
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/Context/InventoryContextState.cs b/AetherBags/Inventory/Context/InventoryContextState.cs
new file mode 100644
index 0000000..1f1e8d6
--- /dev/null
+++ b/AetherBags/Inventory/Context/InventoryContextState.cs
@@ -0,0 +1,157 @@
+using System.Collections.Generic;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
+using FFXIVClientStructs.FFXIV.Client.UI.Misc;
+
+namespace AetherBags.Inventory.Context;
+
+public static unsafe class InventoryContextState
+{
+ private static readonly HashSet<(int page, int slot)> EligibleSlots = new();
+ private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new();
+
+ private static readonly Dictionary VisualLocationMap = new();
+ private static readonly Dictionary> GroupedLocationMaps = new();
+
+ private static uint _lastContextId;
+
+ public static uint ActiveContextId => _lastContextId;
+
+ public static bool HasActiveContext => _lastContextId != 0;
+
+ public static void RefreshMaps()
+ {
+ EligibleSlots.Clear();
+ VisualLocationMap.Clear();
+ GroupedLocationMaps.Clear();
+
+ var itemOrderModule = ItemOrderModule.Instance();
+ if (itemOrderModule == null) return;
+
+ var agentInventory = AgentInventory.Instance();
+ bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0;
+ _lastContextId = hasContext ? agentInventory->OpenTitleId : 0;
+
+ var invArray = hasContext ? InventoryNumberArray.Instance() : null;
+
+ // Helper local to process any sorter
+ void ProcessSorter(ItemOrderModuleSorter* sorter)
+ {
+ if (sorter == null) return;
+
+ // Determine actual page size.
+ // We prefer the physical container size over the sorter's 'ItemsPerPage'
+ var baseInventoryType = sorter->InventoryType;
+ var inventoryManager = InventoryManager.Instance();
+ var container = inventoryManager != null ? inventoryManager->GetInventoryContainer(baseInventoryType) : null;
+
+ // Fallback to sorter value if container isn't loaded, but default to 35 for main/retainer
+ int itemsPerPage = baseInventoryType.UIPageSize;
+ if (itemsPerPage <= 0) itemsPerPage = 35;
+
+ var baseAgentId = (int)baseInventoryType.AgentItemContainerId;
+ if (baseAgentId == 0) return;
+
+ long count = sorter->Items.LongCount;
+ for (int displayIdx = 0; displayIdx < count; displayIdx++)
+ {
+ var entry = sorter->Items[displayIdx].Value;
+ if (entry == null) continue;
+
+ var realContainer = (InventoryType)((int)baseInventoryType + entry->Page);
+ int realSlot = entry->Slot;
+
+ int visualPage = displayIdx / itemsPerPage;
+ int visualSlot = displayIdx % itemsPerPage;
+ int visualContainerId = baseAgentId + visualPage;
+
+ var realKey = new InventoryMappedLocation((int)realContainer, realSlot);
+ var visualValue = new InventoryMappedLocation(visualContainerId, visualSlot);
+
+ VisualLocationMap[realKey] = visualValue;
+
+ if (hasContext && invArray != null && baseInventoryType.IsMainInventory)
+ {
+ var itemData = invArray->Items[displayIdx];
+ if (itemData.IconId != 0)
+ {
+ bool eligible = itemData.ItemFlags.MirageFlag == 0;
+ if (eligible)
+ EligibleSlots.Add(((int)realContainer - (int)InventoryType.Inventory1, realSlot));
+ }
+ }
+ }
+ }
+
+ ProcessSorter(itemOrderModule->InventorySorter);
+
+ ProcessSorter(itemOrderModule->ArmouryMainHandSorter);
+ ProcessSorter(itemOrderModule->ArmouryOffHandSorter);
+ ProcessSorter(itemOrderModule->ArmouryHeadSorter);
+ ProcessSorter(itemOrderModule->ArmouryBodySorter);
+ ProcessSorter(itemOrderModule->ArmouryHandsSorter);
+ ProcessSorter(itemOrderModule->ArmouryLegsSorter);
+ ProcessSorter(itemOrderModule->ArmouryFeetSorter);
+ ProcessSorter(itemOrderModule->ArmouryEarsSorter);
+ ProcessSorter(itemOrderModule->ArmouryNeckSorter);
+ ProcessSorter(itemOrderModule->ArmouryWristsSorter);
+ ProcessSorter(itemOrderModule->ArmouryRingsSorter);
+ ProcessSorter(itemOrderModule->ArmourySoulCrystalSorter);
+
+ ProcessSorter(itemOrderModule->SaddleBagSorter);
+ ProcessSorter(itemOrderModule->PremiumSaddleBagSorter);
+
+ try
+ {
+ var activeRetainerSorter = itemOrderModule->GetActiveRetainerSorter();
+ ProcessSorter(activeRetainerSorter);
+ }
+ catch
+ {
+ // GetActiveRetainerSorter is a member function — guard just in case
+ }
+ }
+
+ public static void RefreshBlockedSlots()
+ {
+ BlockedSlots.Clear();
+
+ var inventoryManager = InventoryManager.Instance();
+ if (inventoryManager == null) return;
+
+ var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems);
+ if (blockedContainer == null) return;
+
+ for (int i = 0; i < blockedContainer->Size; i++)
+ {
+ ref var item = ref blockedContainer->Items[i];
+ if (item.ItemId == 0) continue;
+
+ BlockedSlots.Add((item.Container, item.Slot));
+ }
+ }
+
+ public static bool IsEligible(int page, int slot)
+ => EligibleSlots.Contains((page, slot));
+
+ public static bool IsSlotBlocked(InventoryType container, int slot)
+ => BlockedSlots.Contains((container, slot));
+
+ public static InventoryMappedLocation GetVisualLocation(InventoryType realContainer, int slot)
+ {
+ var key = new InventoryMappedLocation((int)realContainer, slot);
+ if (VisualLocationMap.TryGetValue(key, out var result))
+ return result;
+
+ // default fallback: use the agent container id for the real container (works for Inventory1..4, RetainerPageN, etc.)
+ var defaultAgentId = (int)realContainer.AgentItemContainerId;
+ if (defaultAgentId == 0)
+ {
+ // final fallback: Inventory1 base at 48
+ defaultAgentId = 48;
+ }
+
+ return new InventoryMappedLocation(defaultAgentId, slot);
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryNotificationState.cs b/AetherBags/Inventory/Context/InventoryNotificationState.cs
similarity index 99%
rename from AetherBags/Inventory/InventoryNotificationState.cs
rename to AetherBags/Inventory/Context/InventoryNotificationState.cs
index 6ce636b..02358cb 100644
--- a/AetherBags/Inventory/InventoryNotificationState.cs
+++ b/AetherBags/Inventory/Context/InventoryNotificationState.cs
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Context;
public class InventoryNotificationState
{
diff --git a/AetherBags/Inventory/InventoryContextState.cs b/AetherBags/Inventory/InventoryContextState.cs
deleted file mode 100644
index 88c8a6b..0000000
--- a/AetherBags/Inventory/InventoryContextState.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-using System.Collections.Generic;
-using FFXIVClientStructs.FFXIV.Client.Game;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using FFXIVClientStructs.FFXIV.Client.UI.Arrays;
-using FFXIVClientStructs.FFXIV.Client.UI.Misc;
-
-namespace AetherBags.Inventory;
-
-public static unsafe class InventoryContextState
-{
- private static readonly HashSet<(int page, int slot)> EligibleSlots = new();
- private static readonly HashSet<(InventoryType container, int slot)> BlockedSlots = new();
- private static readonly Dictionary VisualLocationMap = new();
- private static uint _lastContextId;
-
- public static void RefreshMaps()
- {
- EligibleSlots.Clear();
- VisualLocationMap.Clear();
-
- var sorter = ItemOrderModule.Instance()->InventorySorter;
- if (sorter == null) return;
-
- var agentInventory = AgentInventory.Instance();
- bool hasContext = agentInventory != null && agentInventory->OpenTitleId != 0;
- _lastContextId = hasContext ? agentInventory->OpenTitleId : 0;
-
- var invArray = hasContext ? InventoryNumberArray.Instance() : null;
-
- int itemsPerPage = sorter->ItemsPerPage;
-
- for (int displayIdx = 0; displayIdx < 140; displayIdx++)
- {
- var entry = sorter->Items[displayIdx].Value;
- if (entry == null) continue;
-
- int realPage = entry->Page;
- int realSlot = entry->Slot;
-
- int visualPage = displayIdx / itemsPerPage;
- int visualSlot = displayIdx % itemsPerPage;
- int visualContainerId = 48 + visualPage;
-
- VisualLocationMap[new InventoryMappedLocation(realPage, realSlot)] = new InventoryMappedLocation(visualContainerId, visualSlot);
-
- if (hasContext && invArray != null)
- {
- var itemData = invArray->Items[displayIdx];
- if (itemData.IconId == 0) continue;
-
- bool eligible = itemData.ItemFlags.MirageFlag == 0;
- if (eligible)
- {
- EligibleSlots.Add((realPage, realSlot));
- }
- }
- }
- }
-
- public static void RefreshBlockedSlots()
- {
- BlockedSlots.Clear();
-
- var inventoryManager = InventoryManager.Instance();
- if (inventoryManager == null) return;
-
- var blockedContainer = inventoryManager->GetInventoryContainer(InventoryType.BlockedItems);
- if (blockedContainer == null) return;
-
- for (int i = 0; i < blockedContainer->Size; i++)
- {
- ref var item = ref blockedContainer->Items[i];
- if (item.ItemId == 0) continue;
-
- BlockedSlots.Add((item.Container, item.Slot));
- }
- }
-
- public static bool IsEligible(int page, int slot)
- => EligibleSlots.Contains((page, slot));
-
- public static bool IsSlotBlocked(InventoryType container, int slot)
- => BlockedSlots.Contains((container, slot));
-
- public static bool HasActiveContext
- => _lastContextId != 0;
-
- public static InventoryMappedLocation GetVisualLocation(int page, int slot)
- => VisualLocationMap.TryGetValue(new InventoryMappedLocation(page, slot), out var result) ? result : new InventoryMappedLocation(48 + page, slot);
-}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryLocation.cs b/AetherBags/Inventory/InventoryLocation.cs
index 640eec8..7dd5265 100644
--- a/AetherBags/Inventory/InventoryLocation.cs
+++ b/AetherBags/Inventory/InventoryLocation.cs
@@ -17,7 +17,7 @@ public readonly record struct InventoryLocation(InventoryType Container, ushort
public readonly record struct InventoryMappedLocation(int Container, int Slot)
{
- public static readonly InventoryMappedLocation Invalid = new(0, 0);
+ public static readonly InventoryMappedLocation Invalid = new(-1, -1);
public bool IsValid => Container != 0;
diff --git a/AetherBags/Inventory/InventoryOrchestrator.cs b/AetherBags/Inventory/InventoryOrchestrator.cs
new file mode 100644
index 0000000..36c3632
--- /dev/null
+++ b/AetherBags/Inventory/InventoryOrchestrator.cs
@@ -0,0 +1,62 @@
+using System.Collections.Generic;
+using AetherBags.Addons;
+using AetherBags.Inventory.Context;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+
+namespace AetherBags.Inventory;
+
+public static unsafe class InventoryOrchestrator
+{
+ private static readonly InventoryNotificationState NotificationState = new();
+
+ public static void RefreshAll(bool updateMaps = true)
+ {
+ if (updateMaps)
+ {
+ InventoryContextState.RefreshMaps();
+ InventoryContextState.RefreshBlockedSlots();
+ }
+
+ 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 (window.IsOpen)
+ window.ManualRefresh();
+ }
+ });
+ }
+
+ public static void CloseAll()
+ {
+ foreach (var window in GetAllWindows())
+ {
+ window.Close();
+ }
+ }
+
+ public static void RefreshHighlights()
+ {
+ Services.Framework.RunOnTick(() =>
+ {
+ foreach (var window in GetAllWindows())
+ {
+ window.ItemRefresh();
+ }
+ });
+ }
+
+ private static IEnumerable GetAllWindows()
+ {
+ yield return System.AddonInventoryWindow;
+ yield return System.AddonSaddleBagWindow;
+ yield return System.AddonRetainerWindow;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryStats.cs b/AetherBags/Inventory/Items/InventoryStats.cs
similarity index 91%
rename from AetherBags/Inventory/InventoryStats.cs
rename to AetherBags/Inventory/Items/InventoryStats.cs
index 4f9a997..c9d15b5 100644
--- a/AetherBags/Inventory/InventoryStats.cs
+++ b/AetherBags/Inventory/Items/InventoryStats.cs
@@ -1,4 +1,4 @@
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Items;
public readonly struct InventoryStats
{
diff --git a/AetherBags/Inventory/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs
similarity index 70%
rename from AetherBags/Inventory/ItemInfo.cs
rename to AetherBags/Inventory/Items/ItemInfo.cs
index 15e3a7d..b4a59fd 100644
--- a/AetherBags/Inventory/ItemInfo.cs
+++ b/AetherBags/Inventory/Items/ItemInfo.cs
@@ -1,11 +1,12 @@
-using FFXIVClientStructs.FFXIV.Client.Game;
-using Lumina.Excel;
-using Lumina.Excel.Sheets;
using System;
using System.Numerics;
using System.Text.RegularExpressions;
+using AetherBags.Inventory.Context;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using Lumina.Excel;
+using Lumina.Excel.Sheets;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Items;
public sealed class ItemInfo : IEquatable
{
@@ -57,15 +58,13 @@ public sealed class ItemInfo : IEquatable
public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality);
public bool IsDesynthesizable => Row.Desynth > 0;
- public bool IsCraftable => Row.ItemAction.RowId != 0 || Row.CanBeHq; // Simplified check
+ public bool IsCraftable => Row.ItemAction.RowId != 0 || Row.CanBeHq;
public bool IsGlamourable => Row.IsGlamorous;
public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000
private string Description => _description ??= Row.Description.ToString();
- public InventoryMappedLocation VisualLocation =>
- IsMainInventory ? InventoryContextState.GetVisualLocation(InventoryPage, Item.Slot)
- : new InventoryMappedLocation((int)Item.Container.AgentItemContainerId, Item. Slot);
+ public InventoryMappedLocation VisualLocation => InventoryContextState.GetVisualLocation(Item.Container, Item.Slot);
public int InventoryPage => Item.Container switch
@@ -83,13 +82,55 @@ public sealed class ItemInfo : IEquatable
{
get
{
- if (!InventoryContextState.HasActiveContext)
- return true;
+ if (IsSlotBlocked) return false;
+ if (!CheckNativeContextEligibility()) return false;
+ if (!HighlightState.IsInActiveFilters(Item.ItemId)) return false;
- return IsMainInventory && InventoryContextState.IsEligible(InventoryPage, Item.Slot);
+ return true;
}
}
+ public float VisualAlpha => IsEligibleForContext ? 1.0f : 0.4f;
+
+ public Vector3 HighlightOverlayColor
+ {
+ get
+ {
+ if (!System.Config.Categories.BisBuddyEnabled)
+ return Vector3.Zero;
+
+ return HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero;
+ }
+ }
+
+ private bool CheckNativeContextEligibility()
+ {
+ uint contextId = InventoryContextState.ActiveContextId;
+ if (contextId == 0) return true;
+
+ bool isRetainerContext = contextId == 4;
+ bool isSaddlebagContext = contextId == 29;
+ bool isMainContext = !isRetainerContext && isSaddlebagContext == false;
+
+ if (IsMainInventory)
+ {
+ if (!isMainContext) return true;
+ return InventoryContextState.IsEligible(InventoryPage, Item.Slot);
+ }
+
+ if (Item.Container.IsRetainer)
+ {
+ if (!isRetainerContext) return true;
+ }
+
+ if (Item.Container.IsSaddleBag)
+ {
+ if (!isSaddlebagContext) return true;
+ }
+
+ return true;
+ }
+
public bool IsMainInventory => InventoryPage >= 0;
public bool IsRegexMatch(string searchTerms)
diff --git a/AetherBags/Inventory/LootedItemInfo.cs b/AetherBags/Inventory/Items/LootedItemInfo.cs
similarity index 76%
rename from AetherBags/Inventory/LootedItemInfo.cs
rename to AetherBags/Inventory/Items/LootedItemInfo.cs
index 3d4edd9..4e3b1d8 100644
--- a/AetherBags/Inventory/LootedItemInfo.cs
+++ b/AetherBags/Inventory/Items/LootedItemInfo.cs
@@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Items;
public record LootedItemInfo(int Index, InventoryItem Item, int Quantity);
\ No newline at end of file
diff --git a/AetherBags/Inventory/Scanning/AggregatedItem.cs b/AetherBags/Inventory/Scanning/AggregatedItem.cs
new file mode 100644
index 0000000..2f196b2
--- /dev/null
+++ b/AetherBags/Inventory/Scanning/AggregatedItem.cs
@@ -0,0 +1,9 @@
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace AetherBags.Inventory.Scanning;
+
+public struct AggregatedItem
+{
+ public InventoryItem First;
+ public int Total;
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryScanner.cs b/AetherBags/Inventory/Scanning/InventoryScanner.cs
similarity index 66%
rename from AetherBags/Inventory/InventoryScanner.cs
rename to AetherBags/Inventory/Scanning/InventoryScanner.cs
index 00e9d2b..286d698 100644
--- a/AetherBags/Inventory/InventoryScanner.cs
+++ b/AetherBags/Inventory/Scanning/InventoryScanner.cs
@@ -1,19 +1,12 @@
-using AetherBags.Configuration;
-using FFXIVClientStructs.FFXIV.Client.Game;
using System.Collections.Generic;
+using AetherBags.Configuration;
+using AetherBags.Inventory.Items;
+using FFXIVClientStructs.FFXIV.Client.Game;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.Scanning;
public static unsafe class InventoryScanner
{
- private static readonly InventoryType[] BagInventories =
- [
- InventoryType.Inventory1,
- InventoryType.Inventory2,
- InventoryType.Inventory3,
- InventoryType.Inventory4,
- ];
-
public static readonly InventoryType[] StandardInventories =
[
InventoryType.Inventory1,
@@ -46,20 +39,23 @@ public static unsafe class InventoryScanner
public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
=> ((ulong)(uint)container << 32) | (uint)slot;
- public static void ScanBags(
+ public static void ScanInventories(
InventoryManager* inventoryManager,
InventoryStackMode stackMode,
- Dictionary aggByKey)
+ Dictionary aggByKey,
+ InventorySourceType source)
{
aggByKey.Clear();
+ var inventories = InventorySourceDefinitions.GetInventories(source);
+
int scannedSlots = 0;
int nonEmptySlots = 0;
int collisions = 0;
- for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
+ for (int inventoryIndex = 0; inventoryIndex < inventories.Length; inventoryIndex++)
{
- var inventoryType = BagInventories[inventoryIndex];
+ var inventoryType = inventories[inventoryIndex];
var container = inventoryManager->GetInventoryContainer(inventoryType);
if (container == null)
{
@@ -164,16 +160,58 @@ public static unsafe class InventoryScanner
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType);
- public static string GetEmptyItemSlotsString()
+ public static InventoryLocation GetFirstEmptySlot(InventorySourceType source)
{
- uint empty = InventoryManager.Instance()->GetEmptySlotsInBag();
- uint used = 140 - empty;
- return $"{used}/140";
- }
-}
+ var manager = InventoryManager.Instance();
+ var containers = InventorySourceDefinitions.GetContainersForSource(source);
-public struct AggregatedItem
-{
- public InventoryItem First;
- public int Total;
+ foreach (var type in containers)
+ {
+ var container = manager->GetInventoryContainer(type);
+ if (container == null || container->Size == 0) continue;
+
+ for (int i = 0; i < container->Size; i++)
+ {
+ if (container->Items[i].ItemId == 0)
+ return new InventoryLocation(type, (ushort)i);
+ }
+ }
+
+ return InventoryLocation.Invalid;
+ }
+
+ 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;
+ return $"{used}/{total}";
+ }
+
+ private static uint GetEmptySlotsInContainer(InventoryType[] inventories)
+ {
+ uint empty = 0;
+ var inventoryManager = InventoryManager.Instance();
+ foreach (var inv in inventories)
+ {
+ var container = inventoryManager->GetInventoryContainer(inv);
+ var containerSize = container->Size;
+
+ if (container == null) continue;
+ for (int i = 0; i < containerSize; i++)
+ {
+ if (container->Items[i]. ItemId == 0)
+ empty++;
+ }
+ }
+ return empty;
+ }
}
\ No newline at end of file
diff --git a/AetherBags/Inventory/Scanning/InventorySource.cs b/AetherBags/Inventory/Scanning/InventorySource.cs
new file mode 100644
index 0000000..8bdc8a5
--- /dev/null
+++ b/AetherBags/Inventory/Scanning/InventorySource.cs
@@ -0,0 +1,82 @@
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace AetherBags.Inventory.Scanning;
+
+public enum InventorySourceType
+{
+ MainBags,
+ SaddleBag,
+ PremiumSaddleBag,
+ AllSaddleBags,
+ Retainer,
+}
+
+public static class InventorySourceDefinitions
+{
+ public static readonly InventoryType[] MainBags =
+ [
+ InventoryType.Inventory1,
+ InventoryType.Inventory2,
+ InventoryType.Inventory3,
+ InventoryType.Inventory4,
+ ];
+
+ public static readonly InventoryType[] SaddleBag =
+ [
+ InventoryType.SaddleBag1,
+ InventoryType.SaddleBag2,
+ ];
+
+ public static readonly InventoryType[] PremiumSaddleBag =
+ [
+ InventoryType.PremiumSaddleBag1,
+ InventoryType.PremiumSaddleBag2,
+ ];
+
+ public static readonly InventoryType[] AllSaddleBags =
+ [
+ InventoryType.SaddleBag1,
+ InventoryType.SaddleBag2,
+ InventoryType.PremiumSaddleBag1,
+ InventoryType.PremiumSaddleBag2,
+ ];
+
+ public static readonly InventoryType[] Retainer =
+ [
+ InventoryType.RetainerPage1,
+ InventoryType.RetainerPage2,
+ InventoryType.RetainerPage3,
+ InventoryType.RetainerPage4,
+ InventoryType.RetainerPage5,
+ InventoryType.RetainerPage6,
+ InventoryType.RetainerPage7,
+ ];
+
+ public static InventoryType[] GetInventories(InventorySourceType source) => source switch
+ {
+ InventorySourceType.MainBags => MainBags,
+ InventorySourceType.SaddleBag => SaddleBag,
+ InventorySourceType.Retainer => Retainer,
+ _ => MainBags,
+ };
+
+ public static InventoryType[] GetContainersForSource(InventorySourceType source) => source switch
+ {
+ InventorySourceType.MainBags => MainBags,
+ InventorySourceType.SaddleBag => SaddleBag,
+ InventorySourceType.PremiumSaddleBag => PremiumSaddleBag,
+ InventorySourceType.AllSaddleBags => AllSaddleBags,
+ InventorySourceType.Retainer => Retainer,
+ _ => MainBags,
+ };
+
+ public static int GetTotalSlots(InventorySourceType source) => source switch
+ {
+ InventorySourceType.MainBags => 140, // 4 * 35
+ InventorySourceType.SaddleBag => 70, // 2 * 35
+ InventorySourceType.PremiumSaddleBag => 70, // 2 * 35
+ InventorySourceType.AllSaddleBags => 140, // 2 * 35
+ InventorySourceType.Retainer => Retainer.Length * 25, // 7 * 25
+ _ => 140,
+ };
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/InventoryState.cs b/AetherBags/Inventory/State/InventoryState.cs
similarity index 63%
rename from AetherBags/Inventory/InventoryState.cs
rename to AetherBags/Inventory/State/InventoryState.cs
index 1d29106..17bdebc 100644
--- a/AetherBags/Inventory/InventoryState.cs
+++ b/AetherBags/Inventory/State/InventoryState.cs
@@ -1,16 +1,19 @@
+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;
-using System.Collections.Generic;
-using System.Linq;
-namespace AetherBags.Inventory;
+namespace AetherBags.Inventory.State;
public static unsafe class InventoryState
{
- public static IReadOnlyList StandardInventories => InventoryScanner.StandardInventories;
+ private static IReadOnlyList StandardInventories => InventoryScanner.StandardInventories;
private static readonly Dictionary AggByKey = new(capacity: 512);
private static readonly Dictionary ItemInfoByKey = new(capacity: 512);
@@ -28,68 +31,6 @@ public static unsafe class InventoryState
public static bool Contains(this IReadOnlyCollection inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)type);
- public static void RefreshFromGame()
- {
- InventoryManager* inventoryManager = InventoryManager.Instance();
- if (inventoryManager == null)
- {
- ClearAll();
- return;
- }
-
- var config = System.Config;
- InventoryStackMode stackMode = config.General.StackMode;
- bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled;
- bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled;
- List userCategories = config.Categories.UserCategories.Where(category => category.Enabled).ToList();
-
- Services.Logger.DebugOnly($"RefreshFromGame StackMode={stackMode}");
-
- AggByKey.Clear();
- ItemInfoByKey.Clear();
- SortedCategoryKeys.Clear();
- AllCategories.Clear();
- FilteredCategories.Clear();
- ClaimedKeys.Clear();
-
- InventoryScanner.ScanBags(inventoryManager, stackMode, AggByKey);
- CategoryBucketManager.ResetBuckets(BucketsByKey);
- InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey);
- InventoryContextState.RefreshMaps();
- InventoryContextState.RefreshBlockedSlots();
-
- if (userCategoriesEnabled && userCategories.Count > 0)
- {
- CategoryBucketManager.BucketByUserCategories(
- ItemInfoByKey,
- userCategories,
- BucketsByKey,
- ClaimedKeys,
- UserCategoriesSortedScratch);
- }
-
- if (gameCategoriesEnabled)
- {
- CategoryBucketManager.BucketByGameCategories(
- ItemInfoByKey,
- BucketsByKey,
- ClaimedKeys,
- userCategoriesEnabled);
- }
- else
- {
- CategoryBucketManager.BucketUnclaimedToMisc(
- ItemInfoByKey,
- BucketsByKey,
- ClaimedKeys,
- userCategoriesEnabled);
- }
-
- InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch);
- CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys);
- CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories);
- }
-
public static IReadOnlyList GetInventoryItemCategories(string filterString = "", bool invert = false)
{
return InventoryFilter.FilterCategories(
@@ -110,7 +51,7 @@ public static unsafe class InventoryState
totalQuantity += kvp.Value.ItemCount;
}
- uint emptySlots = InventoryManager.Instance()->GetEmptySlotsInBag();
+ uint emptySlots = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag();
const int totalSlots = 140;
var categories = GetInventoryItemCategories(string.Empty);
@@ -126,9 +67,6 @@ public static unsafe class InventoryState
};
}
- public static string GetEmptyItemSlotsString()
- => InventoryScanner.GetEmptyItemSlotsString();
-
public static IReadOnlyList GetCurrencyInfoList(uint[] currencyIds)
=> CurrencyState.GetCurrencyInfoList(currencyIds);
diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs
new file mode 100644
index 0000000..1d8a1af
--- /dev/null
+++ b/AetherBags/Inventory/State/InventoryStateBase.cs
@@ -0,0 +1,149 @@
+using System.Collections.Generic;
+using System.Linq;
+using AetherBags.Configuration;
+using AetherBags.Inventory.Categories;
+using AetherBags.Inventory.Context;
+using AetherBags.Inventory.Items;
+using AetherBags.Inventory.Scanning;
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace AetherBags.Inventory.State;
+
+public abstract class InventoryStateBase
+{
+ protected readonly Dictionary AggByKey = new(capacity: 512);
+ protected readonly Dictionary ItemInfoByKey = new(capacity: 512);
+ protected readonly Dictionary BucketsByKey = new(capacity: 256);
+ protected readonly List SortedCategoryKeys = new(capacity: 256);
+ protected readonly List AllCategories = new(capacity: 256);
+ protected readonly List FilteredCategories = new(capacity: 256);
+ protected readonly List UserCategoriesSortedScratch = new(capacity: 64);
+ protected readonly List RemoveKeysScratch = new(capacity: 256);
+ protected readonly HashSet ClaimedKeys = new(capacity: 512);
+
+ public abstract InventorySourceType SourceType { get; }
+ public abstract InventoryType[] Inventories { get; }
+
+ public virtual unsafe void RefreshFromGame()
+ {
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ if (inventoryManager == null)
+ {
+ ClearAll();
+ return;
+ }
+
+ var config = System.Config;
+ InventoryStackMode stackMode = config.General.StackMode;
+
+ AggByKey.Clear();
+ ItemInfoByKey.Clear();
+ SortedCategoryKeys.Clear();
+ AllCategories.Clear();
+ FilteredCategories.Clear();
+ ClaimedKeys.Clear();
+
+ InventoryScanner.ScanInventories(inventoryManager, stackMode, AggByKey, SourceType);
+ CategoryBucketManager.ResetBuckets(BucketsByKey);
+ InventoryScanner.BuildItemInfos(AggByKey, ItemInfoByKey);
+
+ OnPostScan();
+
+ ApplyCategories(config);
+
+ InventoryScanner.PruneStaleItemInfos(AggByKey, ItemInfoByKey, RemoveKeysScratch);
+ CategoryBucketManager.SortBucketsAndBuildKeyList(BucketsByKey, SortedCategoryKeys);
+ CategoryBucketManager.BuildCategorizedList(BucketsByKey, SortedCategoryKeys, AllCategories);
+ }
+
+ protected virtual void OnPostScan()
+ {
+ }
+
+ protected virtual void ApplyCategories(SystemConfiguration config)
+ {
+ bool categoriesEnabled = config.Categories.CategoriesEnabled;
+ bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled && categoriesEnabled;
+ bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled && categoriesEnabled;
+ bool allaganCategoriesEnabled = config.Categories.AllaganToolsCategoriesEnabled && categoriesEnabled;
+ // TODO: Cache this when config changes
+ var userCategories = config.Categories.UserCategories.Where(c => c.Enabled).ToList();
+
+ if (userCategoriesEnabled && userCategories.Count > 0)
+ {
+ CategoryBucketManager.BucketByUserCategories(
+ ItemInfoByKey, userCategories, BucketsByKey, ClaimedKeys, UserCategoriesSortedScratch);
+ }
+
+ if (allaganCategoriesEnabled)
+ {
+ if (config.Categories.AllaganToolsMode == AllaganToolsFilterMode.Categorize)
+ {
+ CategoryBucketManager.BucketByAllaganFilters(ItemInfoByKey, BucketsByKey, ClaimedKeys, true);
+ HighlightState.ClearFilter(HighlightSource.AllaganTools);
+ }
+ else
+ {
+ UpdateAllaganHighlight(HighlightState.SelectedAllaganToolsFilterKey);
+ }
+ }
+ else
+ {
+ HighlightState.ClearFilter(HighlightSource.AllaganTools);
+ }
+
+ if (gameCategoriesEnabled)
+ {
+ CategoryBucketManager.BucketByGameCategories(
+ ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled);
+ }
+ else
+ {
+ CategoryBucketManager.BucketUnclaimedToMisc(
+ ItemInfoByKey, BucketsByKey, ClaimedKeys, userCategoriesEnabled);
+ }
+ }
+
+ private void UpdateAllaganHighlight(string? filterKey)
+ {
+ if (string.IsNullOrEmpty(filterKey) || !System.IPC.AllaganTools.IsReady)
+ {
+ HighlightState.ClearFilter(HighlightSource.AllaganTools);
+ return;
+ }
+
+ var filterItems = System.IPC.AllaganTools.GetFilterItems(filterKey);
+ if (filterItems != null)
+ {
+ HighlightState.SetFilter(HighlightSource.AllaganTools, filterItems.Keys);
+ }
+ else
+ {
+ HighlightState.ClearFilter(HighlightSource.AllaganTools);
+ }
+ }
+
+ public IReadOnlyList GetCategories(string filter = "", bool invert = false)
+ => InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert);
+
+ public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType);
+
+ protected virtual void ClearAll()
+ {
+ AggByKey.Clear();
+ ItemInfoByKey.Clear();
+
+ foreach (var kvp in BucketsByKey)
+ {
+ kvp.Value.Items.Clear();
+ kvp.Value.FilteredItems.Clear();
+ kvp.Value.Used = false;
+ }
+
+ SortedCategoryKeys.Clear();
+ AllCategories.Clear();
+ FilteredCategories.Clear();
+ RemoveKeysScratch.Clear();
+ ClaimedKeys.Clear();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/State/MainBagState.cs b/AetherBags/Inventory/State/MainBagState.cs
new file mode 100644
index 0000000..945d36a
--- /dev/null
+++ b/AetherBags/Inventory/State/MainBagState.cs
@@ -0,0 +1,17 @@
+using AetherBags.Inventory.Context;
+using AetherBags.Inventory.Scanning;
+using FFXIVClientStructs.FFXIV.Client.Game;
+
+namespace AetherBags.Inventory.State;
+
+public class MainBagState : InventoryStateBase
+{
+ public override InventorySourceType SourceType => InventorySourceType.MainBags;
+ public override InventoryType[] Inventories => InventorySourceDefinitions.MainBags;
+
+ protected override void OnPostScan()
+ {
+ InventoryContextState.RefreshMaps();
+ InventoryContextState.RefreshBlockedSlots();
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/State/RetainerState.cs b/AetherBags/Inventory/State/RetainerState.cs
new file mode 100644
index 0000000..eabe183
--- /dev/null
+++ b/AetherBags/Inventory/State/RetainerState.cs
@@ -0,0 +1,66 @@
+using AetherBags. Inventory.Scanning;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.UI.Agent;
+
+namespace AetherBags. Inventory.State;
+
+public class RetainerState : InventoryStateBase
+{
+ public override InventorySourceType SourceType => InventorySourceType.Retainer;
+ public override InventoryType[] Inventories => InventorySourceDefinitions.Retainer;
+
+
+ public static unsafe ulong CurrentRetainerId
+ {
+ get
+ {
+ var retainerManager = RetainerManager.Instance();
+ if (retainerManager == null) return 0;
+
+ return retainerManager->LastSelectedRetainerId;
+ }
+ }
+
+ public static unsafe string CurrentRetainerName
+ {
+ get
+ {
+ var retainerManager = RetainerManager.Instance();
+ if (retainerManager == null) return string.Empty;
+
+ var retainer = retainerManager->GetActiveRetainer();
+ if (retainer == null) return string.Empty;
+
+ return retainer->NameString;
+ }
+ }
+
+ public static unsafe bool IsRetainerActive
+ {
+ get
+ {
+ if (! Services.ClientState.IsLoggedIn) return false;
+
+ var retainerManager = RetainerManager. Instance();
+ if (retainerManager == null) return false;
+
+ return retainerManager->LastSelectedRetainerId != 0;
+ }
+ }
+
+ public static unsafe bool AreContainersLoaded
+ {
+ get
+ {
+ if (!IsRetainerActive) return false;
+
+ var inventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance();
+ if (inventoryManager == null) return false;
+
+ var container = inventoryManager->GetInventoryContainer(InventoryType.RetainerPage1);
+ return container != null && container->Size > 0;
+ }
+ }
+
+ public static bool CanMoveItems => AreContainersLoaded;
+}
\ No newline at end of file
diff --git a/AetherBags/Inventory/State/SaddleBagState.cs b/AetherBags/Inventory/State/SaddleBagState.cs
new file mode 100644
index 0000000..608d229
--- /dev/null
+++ b/AetherBags/Inventory/State/SaddleBagState.cs
@@ -0,0 +1,27 @@
+using AetherBags.Inventory.Scanning;
+using FFXIVClientStructs.FFXIV.Client.Game;
+using FFXIVClientStructs.FFXIV.Client.Game.UI;
+
+namespace AetherBags.Inventory.State;
+
+public class SaddleBagState : InventoryStateBase
+{
+ public override InventorySourceType SourceType => HasPremiumSaddlebag
+ ? InventorySourceType.AllSaddleBags
+ : InventorySourceType.SaddleBag;
+
+ public override InventoryType[] Inventories => HasPremiumSaddlebag
+ ? InventorySourceDefinitions.AllSaddleBags
+ : InventorySourceDefinitions.SaddleBag;
+
+ private static unsafe bool HasPremiumSaddlebag
+ {
+ get
+ {
+ if (!Services.ClientState.IsLoggedIn) return false;
+
+ var playerState = PlayerState.Instance();
+ return playerState != null && playerState->HasPremiumSaddlebag;
+ }
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Color/ColorInputRow.cs b/AetherBags/Nodes/Color/ColorInputRow.cs
index e4e8535..1eb082f 100644
--- a/AetherBags/Nodes/Color/ColorInputRow.cs
+++ b/AetherBags/Nodes/Color/ColorInputRow.cs
@@ -58,8 +58,8 @@ public class ColorInputRow : HorizontalListNode
if (_colorPickerAddon is not null) return;
_colorPickerAddon = new ColorPickerAddon {
- InternalName = "ColorPicker",
- Title = "ColorPicker_AetherBags",
+ InternalName = "ColorPicker_AetherBags",
+ Title = "Pick a color",
};
}
@@ -94,4 +94,5 @@ public class ColorInputRow : HorizontalListNode
public Action? OnColorConfirmed { get; set; }
public Action? OnColorCanceled { get; set; }
public Action? OnColorChange { get; set; }
+ public Action? OnColorPreviewed { get; set; }
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs
index f2ddd6c..3296573 100644
--- a/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/CategoryConfigurationNode.cs
@@ -5,69 +5,54 @@ using KamiToolKit.Premade.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
-public class CategoryConfigurationNode : ConfigNode
+public class CategoryConfigurationNode : ConfigNode
{
- private readonly ScrollingAreaNode _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode;
public Action? OnCategoryChanged { get; set; }
public CategoryConfigurationNode()
{
- _categoryList = new ScrollingAreaNode
- {
- ContentHeight = 100.0f,
- AutoHideScrollBar = true,
- };
- _categoryList.ContentNode.FitContents = true;
- _categoryList.AttachNode(this);
}
protected override void OptionChanged(CategoryWrapper? option)
{
if (option?.CategoryDefinition is null)
{
- _categoryList.IsVisible = false;
+ if (_activeNode is not null)
+ {
+ _activeNode.IsVisible = false;
+ }
return;
}
- _categoryList.IsVisible = true;
-
if (_activeNode is null)
{
- _activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition)
+ _activeNode = new CategoryDefinitionConfigurationNode
{
- Size = _categoryList.ContentNode.Size,
- OnLayoutChanged = UpdateScrollHeight,
+ OnLayoutChanged = RecalculateLayout,
OnCategoryPropertyChanged = OnCategoryChanged,
};
- _categoryList.ContentNode.AddNode(_activeNode);
- }
- else
- {
- _activeNode.SetCategory(option.CategoryDefinition);
+ _activeNode.AttachNode(this);
}
- UpdateScrollHeight();
+ _activeNode.IsVisible = true;
+ _activeNode.Size = Size;
+ _activeNode.SetCategory(option.CategoryDefinition);
}
- private void UpdateScrollHeight()
+ private void RecalculateLayout()
{
- _categoryList.ContentNode.RecalculateLayout();
- _categoryList.ContentHeight = _categoryList.ContentNode.Height;
+ // Trigger parent layout update if needed
}
protected override void OnSizeChanged()
{
base.OnSizeChanged();
- _categoryList.Size = Size;
- _categoryList.ContentNode.Width = Width;
- foreach (var node in _categoryList.ContentNode.GetNodes())
+ if (_activeNode is not null)
{
- node.Width = Width;
+ _activeNode.Size = Size;
}
-
- UpdateScrollHeight();
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs
index 3b76482..50f1eb8 100644
--- a/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/CategoryDefinitionConfigurationNode.cs
@@ -1,7 +1,9 @@
using System;
+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;
@@ -14,515 +16,500 @@ using Action = System.Action;
namespace AetherBags.Nodes.Configuration.Category;
-public sealed class CategoryDefinitionConfigurationNode : VerticalListNode
+public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode
{
- private readonly CheckboxNode _enabledCheckbox;
- private readonly CheckboxNode _pinnedCheckbox;
- private readonly TextInputNode _nameInputNode;
- private readonly TextInputNode _descriptionInputNode;
- private readonly ColorInputRow _colorInputNode;
- private readonly NumericInputNode _priorityInputNode;
- private readonly NumericInputNode _orderInputNode;
+ private static ExcelSheet- ? ItemSheet => Services.DataManager.GetExcelSheet
- ();
+ private static ExcelSheet? UICategorySheet => Services.DataManager.GetExcelSheet();
- private readonly CheckboxNode _levelEnabledCheckbox;
- private readonly NumericInputNode _levelMinNode;
- private readonly NumericInputNode _levelMaxNode;
+ public Action? OnLayoutChanged { get; init; }
+ public Action? OnCategoryPropertyChanged { get; init; }
- private readonly CheckboxNode _itemLevelEnabledCheckbox;
- private readonly NumericInputNode _itemLevelMinNode;
- private readonly NumericInputNode _itemLevelMaxNode;
+ private UserCategoryDefinition _categoryDefinition = new();
- private readonly CheckboxNode _vendorPriceEnabledCheckbox;
- private readonly NumericInputNode _vendorPriceMinNode;
- private readonly NumericInputNode _vendorPriceMaxNode;
+ private readonly ScrollingAreaNode _scrollingArea;
+ private readonly BasicSettingsSection _basicSettings;
+ private readonly RangeFiltersSection _rangeFilters;
+ private readonly StateFiltersSection _stateFilters;
+ private readonly ListFiltersSection _listFilters;
- private readonly StateFilterRowNode _untradableFilter;
- private readonly StateFilterRowNode _uniqueFilter;
- private readonly StateFilterRowNode _collectableFilter;
- private readonly StateFilterRowNode _dyeableFilter;
- private readonly StateFilterRowNode _repairableFilter;
- private readonly StateFilterRowNode _hqFilter;
- private readonly StateFilterRowNode _desynthFilter;
- private readonly StateFilterRowNode _glamourFilter;
- private readonly StateFilterRowNode _spiritbondFilter;
-
- private readonly UintListEditorNode _allowedItemIdsEditor;
- private readonly StringListEditorNode _allowedNamePatternsEditor;
- private readonly UintListEditorNode _allowedUiCategoriesEditor;
- private readonly RarityEditorNode _allowedRaritiesEditor;
-
- private bool _isInitialized;
-
- private static ExcelSheet
- ? _sItemSheet;
- private static ExcelSheet? _sUICategorySheet;
-
- public Action? OnLayoutChanged { get; set; }
-
- public Action? OnCategoryPropertyChanged { get; set; }
-
- private UserCategoryDefinition CategoryDefinition { get; set; }
-
- public CategoryDefinitionConfigurationNode(UserCategoryDefinition categoryDefinition)
+ public CategoryDefinitionConfigurationNode()
{
- CategoryDefinition = categoryDefinition;
-
- _sItemSheet ??= Services.DataManager.GetExcelSheet
- ();
- _sUICategorySheet ??= Services.DataManager.GetExcelSheet();
-
- FitContents = true;
- ItemSpacing = 4.0f;
-
- var catchAllWarningNode = new TextNode
+ _scrollingArea = new ScrollingAreaNode
{
- Size = new Vector2(300, 40),
- TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize,
- SeString = new SeStringBuilder().Append(" Warning: No rules configured\nThis category won't match anything!").ToReadOnlySeString(),
- TextColor = ColorHelper.GetColor(17),
- LineSpacing = 20,
- IsVisible = UserCategoryMatcher.IsCatchAll(CategoryDefinition),
+ ContentHeight = 100.0f,
+ AutoHideScrollBar = true,
};
- AddNode(catchAllWarningNode);
+ _scrollingArea.AttachNode(this);
- AddNode(CreateSectionHeader("Basic Settings"));
+ _scrollingArea.ContentNode.OnLayoutUpdate = newHeight =>
+ {
+ _scrollingArea.ContentHeight = newHeight;
+ };
+
+ _scrollingArea.ContentNode.CategoryVerticalSpacing = 4.0f;
+
+ var treeListNode = _scrollingArea.ContentAreaNode;
+
+ _basicSettings = new BasicSettingsSection(() => _categoryDefinition)
+ {
+ String = "Basic Settings",
+ IsCollapsed = false,
+ OnPropertyChanged = () =>
+ {
+ NotifyChanged();
+ NotifyCategoryPropertyChanged();
+ },
+ OnValueChanged = NotifyChanged,
+ };
+ _basicSettings.OnToggle = _ => HandleLayoutChange();
+ treeListNode.AddCategoryNode(_basicSettings);
+
+ _rangeFilters = new RangeFiltersSection(() => _categoryDefinition)
+ {
+ String = "Range Filters",
+ IsCollapsed = true,
+ OnValueChanged = NotifyChanged,
+ };
+ _rangeFilters.OnToggle = _ => HandleLayoutChange();
+ treeListNode.AddCategoryNode(_rangeFilters);
+
+ _stateFilters = new StateFiltersSection(() => _categoryDefinition)
+ {
+ String = "State Filters",
+ IsCollapsed = true,
+ OnValueChanged = NotifyChanged,
+ };
+ _stateFilters.OnToggle = _ => HandleLayoutChange();
+ treeListNode.AddCategoryNode(_stateFilters);
+
+ _listFilters = new ListFiltersSection(() => _categoryDefinition)
+ {
+ String = "List Filters",
+ IsCollapsed = true,
+ OnValueChanged = NotifyChanged,
+ OnListChanged = HandleListChanged,
+ };
+ _listFilters.OnToggle = _ => HandleLayoutChange();
+ treeListNode.AddCategoryNode(_listFilters);
+ }
+
+ protected override void OnSizeChanged()
+ {
+ base.OnSizeChanged();
+
+ _scrollingArea.Size = Size;
+
+ foreach (var categoryNode in _scrollingArea.ContentNode.CategoryNodes)
+ {
+ categoryNode.Width = Width - 16.0f;
+ }
+
+ _scrollingArea.ContentNode.RefreshLayout();
+ }
+
+ public void SetCategory(UserCategoryDefinition newCategory)
+ {
+ _categoryDefinition = newCategory;
+ RefreshAllValues();
+ }
+
+ private void RefreshAllValues()
+ {
+ _basicSettings.Refresh();
+ _rangeFilters.Refresh();
+ _stateFilters.Refresh();
+ _listFilters.Refresh();
+
+ HandleLayoutChange();
+ }
+
+ private void HandleListChanged()
+ {
+ NotifyChanged();
+ HandleLayoutChange();
+ }
+
+ private void HandleLayoutChange()
+ {
+ _scrollingArea.ContentNode.RefreshLayout();
+ OnLayoutChanged?.Invoke();
+ }
+
+ private static void NotifyChanged() => InventoryOrchestrator.RefreshAll(updateMaps: true);
+
+ private void NotifyCategoryPropertyChanged() => OnCategoryPropertyChanged?.Invoke();
+
+ public static string ResolveItemName(uint itemId) => ItemSheet?.GetRow(itemId).Name.ToString() ?? "Unknown";
+
+ public static string ResolveUiCategoryName(uint categoryId) => UICategorySheet?.GetRow(categoryId).Name.ToString() ?? "Unknown";
+}
+
+public abstract class ConfigurationSection : TreeListCategoryNode
+{
+ private readonly Func _getCategoryDefinition;
+
+ public Action? OnValueChanged { get; init; }
+
+ protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition();
+
+ protected ConfigurationSection(Func getCategoryDefinition)
+ {
+ _getCategoryDefinition = getCategoryDefinition;
+ VerticalPadding = 4.0f;
+ }
+
+ protected static LabelTextNode CreateLabel(string text) => new()
+ {
+ TextFlags = TextFlags.AutoAdjustNodeSize,
+ Size = new Vector2(80, 20),
+ String = text,
+ };
+}
+
+public sealed class BasicSettingsSection : ConfigurationSection
+{
+ public Action? OnPropertyChanged { get; init; }
+
+ private CheckboxNode? _enabledCheckbox;
+ private CheckboxNode? _pinnedCheckbox;
+ private TextInputNode? _nameInput;
+ private TextInputNode? _descriptionInput;
+ private ColorInputRow? _colorInput;
+ private NumericInputNode? _priorityInput;
+ private NumericInputNode? _orderInput;
+
+ private bool _initialized;
+
+ public BasicSettingsSection(Func getCategoryDefinition)
+ : base(getCategoryDefinition)
+ {
+ }
+
+ private void EnsureInitialized()
+ {
+ if (_initialized) return;
+ _initialized = true;
_enabledCheckbox = new CheckboxNode
{
- Size = new Vector2(200, 20),
+ Size = new Vector2(Width, 20),
String = "Enabled",
- IsChecked = CategoryDefinition.Enabled,
OnClick = isChecked =>
{
CategoryDefinition.Enabled = isChecked;
- NotifyChanged();
- NotifyCategoryPropertyChanged();
+ OnPropertyChanged?.Invoke();
},
};
AddNode(_enabledCheckbox);
_pinnedCheckbox = new CheckboxNode
{
- Size = new Vector2(200, 20),
+ Size = new Vector2(Width, 20),
String = "Pinned",
- IsChecked = CategoryDefinition.Pinned,
OnClick = isChecked =>
{
CategoryDefinition.Pinned = isChecked;
- NotifyChanged();
- NotifyCategoryPropertyChanged();
+ OnPropertyChanged?.Invoke();
},
};
AddNode(_pinnedCheckbox);
- AddNode(new LabelTextNode
- {
- TextFlags = TextFlags.AutoAdjustNodeSize,
- Size = new Vector2(80, 20),
- String = "Name:"
- });
- _nameInputNode = new TextInputNode
+ AddNode(CreateLabel("Name: "));
+ _nameInput = new TextInputNode
{
Size = new Vector2(250, 28),
- String = CategoryDefinition.Name,
- PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "",
- OnInputReceived = name =>
+ PlaceholderString = "Category Name",
+ OnInputReceived = input =>
{
- CategoryDefinition.Name = name.ExtractText();
- NotifyChanged();
- NotifyCategoryPropertyChanged();
+ CategoryDefinition.Name = input.ExtractText();
+ OnPropertyChanged?.Invoke();
},
};
- AddNode(_nameInputNode);
+ AddNode(_nameInput);
- AddNode(new LabelTextNode
- {
- TextFlags = TextFlags.AutoAdjustNodeSize,
- Size = new Vector2(80, 20),
- String = "Description:"
- });
- _descriptionInputNode = new TextInputNode
+ AddNode(CreateLabel("Description:"));
+ _descriptionInput = new TextInputNode
{
Size = new Vector2(250, 28),
- String = CategoryDefinition.Description,
- PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "",
- OnInputReceived = desc =>
+ PlaceholderString = "Optional description",
+ OnInputReceived = input =>
{
- CategoryDefinition.Description = desc.ExtractText();
- NotifyChanged();
+ CategoryDefinition.Description = input.ExtractText();
+ OnValueChanged?.Invoke();
},
};
- AddNode(_descriptionInputNode);
+ AddNode(_descriptionInput);
- _colorInputNode = new ColorInputRow
+ _colorInput = new ColorInputRow
{
Label = "Color",
Size = new Vector2(300, 28),
- CurrentColor = CategoryDefinition.Color,
+ CurrentColor = new UserCategoryDefinition().Color,
DefaultColor = new UserCategoryDefinition().Color,
- OnColorConfirmed = color =>
- {
- CategoryDefinition.Color = color;
- NotifyChanged();
- },
- OnColorCanceled = color =>
- {
- CategoryDefinition.Color = color;
- NotifyChanged();
- },
+ OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
+ OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
+ OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
};
- AddNode(_colorInputNode);
+ AddNode(_colorInput);
- AddNode(new LabelTextNode
- {
- TextFlags = TextFlags.AutoAdjustNodeSize,
- Size = new Vector2(80, 20),
- String = "Priority:"
- });
- _priorityInputNode = new NumericInputNode
+ AddNode(CreateLabel("Priority:"));
+ _priorityInput = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = 0,
Max = 1000,
Step = 1,
- Value = CategoryDefinition.Priority,
OnValueUpdate = val =>
{
CategoryDefinition.Priority = val;
- NotifyChanged();
+ OnValueChanged?.Invoke();
},
};
- AddNode(_priorityInputNode);
+ AddNode(_priorityInput);
- AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = "Order:" });
- _orderInputNode = new NumericInputNode
+ AddNode(CreateLabel("Order: "));
+ _orderInput = new NumericInputNode
{
Size = new Vector2(120, 28),
Min = 0,
Max = 9999,
Step = 1,
- Value = CategoryDefinition.Order,
OnValueUpdate = val =>
{
CategoryDefinition.Order = val;
- NotifyChanged();
- NotifyCategoryPropertyChanged();
+ OnPropertyChanged?.Invoke();
},
};
- AddNode(_orderInputNode);
+ AddNode(_orderInput);
- AddNode(CreateSectionHeader("Range Filters"));
+ RecalculateLayout();
+ }
- (_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter(
- "Level",
- CategoryDefinition.Rules.Level,
- 0, 200,
- (enabled, min, max) =>
+ public void Refresh()
+ {
+ EnsureInitialized();
+
+ _enabledCheckbox!.IsChecked = CategoryDefinition.Enabled;
+ _pinnedCheckbox!.IsChecked = CategoryDefinition.Pinned;
+ _nameInput!.String = CategoryDefinition.Name;
+ _nameInput.PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "";
+ _descriptionInput!.String = CategoryDefinition.Description;
+ _descriptionInput.PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "";
+ _colorInput!.CurrentColor = CategoryDefinition.Color;
+ _priorityInput!.Value = CategoryDefinition.Priority;
+ _orderInput!.Value = CategoryDefinition.Order;
+
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ }
+}
+
+public sealed class RangeFiltersSection : ConfigurationSection
+{
+ private RangeFilterRow? _levelFilter;
+ private RangeFilterRow? _itemLevelFilter;
+ private RangeFilterRowUint? _vendorPriceFilter;
+
+ private bool _initialized;
+
+ public RangeFiltersSection(Func getCategoryDefinition)
+ : base(getCategoryDefinition)
+ {
+ }
+
+ private void EnsureInitialized()
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ _levelFilter = new RangeFilterRow
+ {
+ Label = "Level",
+ MinBound = 0,
+ MaxBound = 200,
+ OnFilterChanged = (enabled, min, max) =>
{
CategoryDefinition.Rules.Level.Enabled = enabled;
CategoryDefinition.Rules.Level.Min = min;
CategoryDefinition.Rules.Level.Max = max;
- NotifyChanged();
- }
- );
+ OnValueChanged?.Invoke();
+ },
+ };
+ AddNode(_levelFilter);
- (_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter(
- "Item Level",
- CategoryDefinition.Rules.ItemLevel,
- 0, 2000,
- (enabled, min, max) =>
+ _itemLevelFilter = new RangeFilterRow
+ {
+ Label = "Item Level",
+ MinBound = 0,
+ MaxBound = 2000,
+ OnFilterChanged = (enabled, min, max) =>
{
CategoryDefinition.Rules.ItemLevel.Enabled = enabled;
CategoryDefinition.Rules.ItemLevel.Min = min;
CategoryDefinition.Rules.ItemLevel.Max = max;
- NotifyChanged();
- }
- );
-
- (_vendorPriceEnabledCheckbox, _vendorPriceMinNode, _vendorPriceMaxNode) = CreateRangeFilterUint(
- "Vendor Price",
- CategoryDefinition.Rules.VendorPrice,
- 0, 9_999_999
- );
-
- AddNode(CreateSectionHeader("State Filters"));
-
- _untradableFilter = new StateFilterRowNode("Untradable", CategoryDefinition.Rules.Untradable, NotifyChanged);
- AddNode(_untradableFilter);
-
- _uniqueFilter = new StateFilterRowNode("Unique", CategoryDefinition.Rules.Unique, NotifyChanged);
- AddNode(_uniqueFilter);
-
- _collectableFilter = new StateFilterRowNode("Collectable", CategoryDefinition.Rules.Collectable, NotifyChanged);
- AddNode(_collectableFilter);
-
- _dyeableFilter = new StateFilterRowNode("Dyeable", CategoryDefinition.Rules.Dyeable, NotifyChanged);
- AddNode(_dyeableFilter);
-
- _repairableFilter = new StateFilterRowNode("Repairable", CategoryDefinition.Rules.Repairable, NotifyChanged);
- AddNode(_repairableFilter);
-
- _hqFilter = new StateFilterRowNode("High Quality", CategoryDefinition.Rules.HighQuality, NotifyChanged);
- AddNode(_hqFilter);
-
- _desynthFilter = new StateFilterRowNode("Desynthesizable", CategoryDefinition.Rules.Desynthesizable, NotifyChanged);
- AddNode(_desynthFilter);
-
- _glamourFilter = new StateFilterRowNode("Glamourable", CategoryDefinition.Rules.Glamourable, NotifyChanged);
- AddNode(_glamourFilter);
-
- _spiritbondFilter = new StateFilterRowNode("Spiritbonded", CategoryDefinition.Rules.FullySpiritbonded, NotifyChanged);
- AddNode(_spiritbondFilter);
-
- AddNode(CreateSectionHeader("List Filters"));
-
- _allowedItemIdsEditor = new UintListEditorNode(
- "Allowed Item IDs:",
- CategoryDefinition.Rules.AllowedItemIds,
- OnListChanged,
- ResolveItemName
- );
- AddNode(_allowedItemIdsEditor);
-
- _allowedNamePatternsEditor = new StringListEditorNode(
- "Name Patterns (Regex):",
- CategoryDefinition.Rules.AllowedItemNamePatterns,
- OnListChanged
- );
- AddNode(_allowedNamePatternsEditor);
-
- _allowedUiCategoriesEditor = new UintListEditorNode(
- "UI Categories:",
- CategoryDefinition.Rules.AllowedUiCategoryIds,
- OnListChanged,
- ResolveUiCategoryName
- );
- AddNode(_allowedUiCategoriesEditor);
-
- _allowedRaritiesEditor = new RarityEditorNode(
- CategoryDefinition.Rules.AllowedRarities,
- NotifyChanged
- );
- AddNode(_allowedRaritiesEditor);
-
- _isInitialized = true;
- }
-
- private void OnListChanged()
- {
- NotifyChanged();
- RecalculateLayout();
- OnLayoutChanged?.Invoke();
- }
-
- private static string ResolveItemName(uint itemId)
- {
- try
- {
- var item = _sItemSheet?.GetRow(itemId);
- return item?.Name.ToString() ?? "Unknown";
- }
- catch
- {
- return "Unknown";
- }
- }
-
- private static string ResolveUiCategoryName(uint categoryId)
- {
- try
- {
- var category = _sUICategorySheet?.GetRow(categoryId);
- return category?.Name.ToString() ?? "Unknown";
- }
- catch
- {
- return "Unknown";
- }
- }
-
- private static void NotifyChanged()
- {
- System.AddonInventoryWindow.ManualInventoryRefresh();
- }
-
- private void NotifyCategoryPropertyChanged()
- {
- OnCategoryPropertyChanged?.Invoke();
- }
-
- private static LabelTextNode CreateSectionHeader(string text)
- {
- return new LabelTextNode
- {
- Size = new Vector2(300, 22),
- String = text,
- TextColor = ColorHelper.GetColor(2),
- TextOutlineColor = ColorHelper.GetColor(0),
+ OnValueChanged?.Invoke();
+ },
};
- }
+ AddNode(_itemLevelFilter);
- private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter(
- string label,
- RangeFilter filter,
- int minBound,
- int maxBound,
- Action onUpdate)
- {
- var enabledCheckbox = new CheckboxNode
+ _vendorPriceFilter = new RangeFilterRowUint
{
- Size = new Vector2(200, 20),
- String = $"{label} Filter",
- IsChecked = filter.Enabled,
+ Label = "Vendor Price",
+ MinBound = 0,
+ MaxBound = 9_999_999,
+ OnFilterChanged = (enabled, min, max) =>
+ {
+ CategoryDefinition.Rules.VendorPrice.Enabled = enabled;
+ CategoryDefinition.Rules.VendorPrice.Min = min;
+ CategoryDefinition.Rules.VendorPrice.Max = max;
+ OnValueChanged?.Invoke();
+ },
};
- AddNode(enabledCheckbox);
-
- var minNode = new NumericInputNode
- {
- Size = new Vector2(120, 28),
- Min = minBound,
- Max = maxBound,
- Value = filter.Min,
- IsEnabled = filter.Enabled,
- };
-
- var maxNode = new NumericInputNode
- {
- Size = new Vector2(120, 28),
- Min = minBound,
- Max = maxBound,
- Value = filter.Max,
- IsEnabled = filter.Enabled,
- };
-
- var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
- rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" });
- rangeRow.AddNode(minNode);
- rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" });
- rangeRow.AddNode(maxNode);
- AddNode(rangeRow);
-
- enabledCheckbox.OnClick = isChecked =>
- {
- minNode.IsEnabled = isChecked;
- maxNode.IsEnabled = isChecked;
- onUpdate(isChecked, minNode.Value, maxNode.Value);
- };
-
- minNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, val, maxNode.Value);
- maxNode.OnValueUpdate = val => onUpdate(enabledCheckbox.IsChecked, minNode.Value, val);
-
- return (enabledCheckbox, minNode, maxNode);
- }
-
- private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilterUint(
- string label,
- RangeFilter filter,
- int minBound,
- int maxBound)
- {
- var enabledCheckbox = new CheckboxNode
- {
- Size = new Vector2(200, 20),
- String = $"{label} Filter",
- IsChecked = filter.Enabled,
- };
- AddNode(enabledCheckbox);
-
- var minNode = new NumericInputNode
- {
- Size = new Vector2(120, 28),
- Min = minBound,
- Max = maxBound,
- Value = (int)filter.Min,
- IsEnabled = filter.Enabled,
- };
-
- var maxNode = new NumericInputNode
- {
- Size = new Vector2(120, 28),
- Min = minBound,
- Max = maxBound,
- Value = (int)Math.Min(filter.Max, maxBound),
- IsEnabled = filter.Enabled,
- };
-
- var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
- rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Min:" });
- rangeRow.AddNode(minNode);
- rangeRow.AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(30, 28), String = "Max:" });
- rangeRow.AddNode(maxNode);
- AddNode(rangeRow);
-
- enabledCheckbox.OnClick = isChecked =>
- {
- minNode.IsEnabled = isChecked;
- maxNode.IsEnabled = isChecked;
- CategoryDefinition.Rules.VendorPrice.Enabled = isChecked;
- NotifyChanged();
- };
-
- minNode.OnValueUpdate = value =>
- {
- CategoryDefinition.Rules.VendorPrice.Min = (uint)value;
- NotifyChanged();
- };
-
- maxNode.OnValueUpdate = value =>
- {
- CategoryDefinition.Rules.VendorPrice.Max = (uint)value;
- NotifyChanged();
- };
-
- return (enabledCheckbox, minNode, maxNode);
- }
-
- public void SetCategory(UserCategoryDefinition newCategory)
- {
- CategoryDefinition = newCategory;
- RefreshValues();
- }
-
- private void RefreshValues()
- {
- if (! _isInitialized) return;
-
- _enabledCheckbox.IsChecked = CategoryDefinition.Enabled;
- _pinnedCheckbox.IsChecked = CategoryDefinition.Pinned;
- _colorInputNode.CurrentColor = CategoryDefinition.Color;
- _nameInputNode.String = CategoryDefinition.Name;
- _descriptionInputNode.String = CategoryDefinition.Description;
- _priorityInputNode.Value = CategoryDefinition.Priority;
- _orderInputNode.Value = CategoryDefinition.Order;
-
- RefreshRangeFilter(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode, CategoryDefinition.Rules.Level);
- RefreshRangeFilter(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode, CategoryDefinition.Rules.ItemLevel);
-
- _vendorPriceEnabledCheckbox.IsChecked = CategoryDefinition.Rules.VendorPrice.Enabled;
- _vendorPriceMinNode.Value = (int)CategoryDefinition.Rules.VendorPrice.Min;
- _vendorPriceMaxNode.Value = (int)Math.Min(CategoryDefinition.Rules.VendorPrice.Max, int.MaxValue);
- _vendorPriceMinNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled;
- _vendorPriceMaxNode.IsEnabled = CategoryDefinition.Rules.VendorPrice.Enabled;
-
- _untradableFilter.SetState(CategoryDefinition.Rules.Untradable);
- _uniqueFilter.SetState(CategoryDefinition.Rules.Unique);
- _collectableFilter.SetState(CategoryDefinition.Rules.Collectable);
- _dyeableFilter.SetState(CategoryDefinition.Rules.Dyeable);
- _repairableFilter.SetState(CategoryDefinition.Rules.Repairable);
-
- _allowedItemIdsEditor.SetList(CategoryDefinition.Rules.AllowedItemIds);
- _allowedNamePatternsEditor.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns);
- _allowedUiCategoriesEditor.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds);
- _allowedRaritiesEditor.SetList(CategoryDefinition.Rules.AllowedRarities);
+ AddNode(_vendorPriceFilter);
RecalculateLayout();
- OnLayoutChanged?.Invoke();
}
- private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter filter)
+ public void Refresh()
{
- enabled.IsChecked = filter.Enabled;
- min.Value = filter.Min;
- max.Value = filter.Max;
- min.IsEnabled = filter.Enabled;
- max.IsEnabled = filter.Enabled;
+ EnsureInitialized();
+
+ _levelFilter!.SetFilter(CategoryDefinition.Rules.Level);
+ _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel);
+ _vendorPriceFilter!.SetFilter(CategoryDefinition.Rules.VendorPrice);
+
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ }
+}
+
+public sealed class StateFiltersSection : ConfigurationSection
+{
+ private readonly List<(StateFilterRowNode Node, Func GetFilter)> _filters = [];
+ private bool _initialized;
+
+ public StateFiltersSection(Func getCategoryDefinition)
+ : base(getCategoryDefinition)
+ {
+ }
+
+ private void EnsureInitialized()
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ AddFilter("Untradable", def => def.Rules.Untradable);
+ AddFilter("Unique", def => def.Rules.Unique);
+ AddFilter("Collectable", def => def.Rules.Collectable);
+ AddFilter("Dyeable", def => def.Rules.Dyeable);
+ AddFilter("Repairable", def => def.Rules.Repairable);
+ AddFilter("High Quality", def => def.Rules.HighQuality);
+ AddFilter("Desynthesizable", def => def.Rules.Desynthesizable);
+ AddFilter("Glamourable", def => def.Rules.Glamourable);
+ AddFilter("Spiritbonded", def => def.Rules.FullySpiritbonded);
+
+ RecalculateLayout();
+ }
+
+ private void AddFilter(string label, Func getFilter)
+ {
+ var node = new StateFilterRowNode(label, new StateFilter(), () => OnValueChanged?.Invoke());
+ _filters.Add((node, getFilter));
+ AddNode(node);
+ }
+
+ public void Refresh()
+ {
+ EnsureInitialized();
+
+ foreach (var (node, getFilter) in _filters)
+ {
+ node.SetState(getFilter(CategoryDefinition));
+ }
+
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ }
+}
+
+public sealed class ListFiltersSection : ConfigurationSection
+{
+ public Action? OnListChanged { get; init; }
+
+ private UintListEditorNode? _itemIdsEditor;
+ private StringListEditorNode? _namePatternsEditor;
+ private UintListEditorNode? _uiCategoriesEditor;
+ private RarityEditorNode? _raritiesEditor;
+
+ private bool _initialized;
+
+ public ListFiltersSection(Func getCategoryDefinition)
+ : base(getCategoryDefinition)
+ {
+ }
+
+ private void EnsureInitialized()
+ {
+ if (_initialized) return;
+ _initialized = true;
+
+ _itemIdsEditor = new UintListEditorNode
+ {
+ Label = "Allowed Item IDs:",
+ LabelResolver = CategoryDefinitionConfigurationNode.ResolveItemName,
+ OnChanged = () =>
+ {
+ OnListChanged?.Invoke();
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ },
+ };
+ AddNode(_itemIdsEditor);
+
+ _namePatternsEditor = new StringListEditorNode
+ {
+ Label = "Name Patterns (Regex):",
+ OnChanged = () =>
+ {
+ OnListChanged?.Invoke();
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ },
+ };
+ AddNode(_namePatternsEditor);
+
+ _uiCategoriesEditor = new UintListEditorNode
+ {
+ Label = "UI Categories:",
+ LabelResolver = CategoryDefinitionConfigurationNode.ResolveUiCategoryName,
+ OnChanged = () =>
+ {
+ OnListChanged?.Invoke();
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
+ },
+ };
+ AddNode(_uiCategoriesEditor);
+
+ _raritiesEditor = new RarityEditorNode
+ {
+ OnChanged = () => OnValueChanged?.Invoke(),
+ };
+ AddNode(_raritiesEditor);
+
+ RecalculateLayout();
+ }
+
+ public void Refresh()
+ {
+ EnsureInitialized();
+
+ _itemIdsEditor!.SetList(CategoryDefinition.Rules.AllowedItemIds);
+ _namePatternsEditor!.SetList(CategoryDefinition.Rules.AllowedItemNamePatterns);
+ _uiCategoriesEditor!.SetList(CategoryDefinition.Rules.AllowedUiCategoryIds);
+ _raritiesEditor!.SetList(CategoryDefinition.Rules.AllowedRarities);
+
+ RecalculateLayout();
+ ParentTreeListNode?.RefreshLayout();
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs
new file mode 100644
index 0000000..4752f5f
--- /dev/null
+++ b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs
@@ -0,0 +1,148 @@
+using System;
+using System.Linq;
+using System.Numerics;
+using AetherBags.Configuration;
+using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
+using AetherBags.Nodes.Color;
+using AetherBags.Nodes.Input;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes.Configuration.Category;
+
+public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
+{
+ private readonly CheckboxNode _allaganToolsCheckbox;
+ public CategoryGeneralConfigurationNode()
+ {
+ CategorySettings config = System.Config.Categories;
+
+ ItemVerticalSpacing = 2;
+
+ LabelTextNode titleNode = new LabelTextNode
+ {
+ Size = Size with { Y = 18 },
+ String = "Category Configuration",
+ TextColor = ColorHelper.GetColor(2),
+ TextOutlineColor = ColorHelper.GetColor(0),
+ };
+ AddNode(titleNode);
+
+ AddTab(1);
+
+ CheckboxNode categoriesEnabled = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Categories Enabled",
+ IsChecked = config.CategoriesEnabled,
+ OnClick = isChecked =>
+ {
+ config.CategoriesEnabled = isChecked;
+ RefreshInventory();
+ }
+ };
+ AddNode(categoriesEnabled);
+
+ AddTab(1);
+
+ CheckboxNode gameCategoriesEnabled = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Game Categories",
+ IsChecked = config.GameCategoriesEnabled,
+ TextTooltip = "Use the game's built-in item categories (e.g., Arms, Tools, Armor).",
+ OnClick = isChecked =>
+ {
+ config.GameCategoriesEnabled = isChecked;
+ RefreshInventory();
+ }
+ };
+ AddNode(gameCategoriesEnabled);
+
+ CheckboxNode userCategoriesEnabled = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "User Categories",
+ IsChecked = config.UserCategoriesEnabled,
+ TextTooltip = "Use your custom-defined categories.",
+ OnClick = isChecked =>
+ {
+ config.UserCategoriesEnabled = isChecked;
+ RefreshInventory();
+ }
+ };
+ AddNode(userCategoriesEnabled);
+
+ bool bisBuddyReady = System.IPC.BisBuddy?.IsReady ?? false;
+
+ CheckboxNode bisBuddyEnabled = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = bisBuddyReady ? "BISBuddy" : "BISBuddy (Not Available)",
+ IsChecked = config.BisBuddyEnabled,
+ TextTooltip = "Allow BISBuddy to highlight items.",
+ OnClick = isChecked =>
+ {
+ config.BisBuddyEnabled = isChecked;
+ System.IPC.BisBuddy?.RequestUpdate();
+ RefreshInventory();
+ }
+ };
+ AddNode(bisBuddyEnabled);
+
+ bool allaganReady = System.IPC.AllaganTools?.IsReady ?? false;
+
+ LabeledDropdownNode? atModeDropdown = new LabeledDropdownNode
+ {
+ Size = new Vector2(300, 20),
+ LabelText = "Filter Display Mode",
+ LabelTextFlags = TextFlags.AutoAdjustNodeSize,
+ IsEnabled = config.AllaganToolsCategoriesEnabled && allaganReady,
+ Options = Enum.GetNames(typeof(AllaganToolsFilterMode)).ToList(),
+ SelectedOption = config.AllaganToolsMode.ToString(),
+ OnOptionSelected = selected =>
+ {
+ if (Enum.TryParse(selected, out var parsed))
+ {
+ config.AllaganToolsMode = parsed;
+ if (parsed == AllaganToolsFilterMode.Categorize)
+ HighlightState.ClearFilter(HighlightSource.AllaganTools);
+
+ RefreshInventory();
+ }
+ }
+ };
+
+ _allaganToolsCheckbox = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = allaganReady ? "Allagan Tools Filters" : "Allagan Tools Filters (Not Available)",
+ IsChecked = config.AllaganToolsCategoriesEnabled,
+ IsEnabled = allaganReady,
+ TextTooltip = allaganReady
+ ? "Use search filters from Allagan Tools as categories. Items matching a filter will be grouped together."
+ : "Allagan Tools is not installed or not initialized.",
+ OnClick = isChecked =>
+ {
+ config.AllaganToolsCategoriesEnabled = isChecked;
+ if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked;
+ if (isChecked) System.IPC?.AllaganTools?.RefreshFilters();
+ RefreshInventory();
+ }
+ };
+ AddNode(_allaganToolsCheckbox);
+
+ AddTab(1);
+ AddNode(atModeDropdown);
+ SubtractTab(1);
+ }
+
+ private void RefreshInventory() => InventoryOrchestrator.RefreshAll(updateMaps: true);
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs
index 7d489c9..d6b0ef5 100644
--- a/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/CategoryScrollingAreaNode.cs
@@ -4,7 +4,7 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
-public class CategoryScrollingAreaNode : ScrollingAreaNode
+public sealed class CategoryScrollingAreaNode : ScrollingListNode
{
private AddonCategoryConfigurationWindow? _categoryConfigurationAddon;
private readonly TextButtonNode _categoryConfigurationButtonNode;
@@ -13,13 +13,15 @@ public class CategoryScrollingAreaNode : ScrollingAreaNode
{
InitializeCategoryAddon();
+ AddNode(new CategoryGeneralConfigurationNode());
+
_categoryConfigurationButtonNode = new TextButtonNode
{
Size = new Vector2(300, 28),
String = "Configure Categories",
OnClick = () => _categoryConfigurationAddon?.Toggle(),
};
- _categoryConfigurationButtonNode.AttachNode(this);
+ AddNode(_categoryConfigurationButtonNode);
}
private void InitializeCategoryAddon() {
diff --git a/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs
new file mode 100644
index 0000000..9304a42
--- /dev/null
+++ b/AetherBags/Nodes/Configuration/Category/RangeFilterRow.cs
@@ -0,0 +1,206 @@
+using System;
+using System.Numerics;
+using AetherBags.Configuration;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes.Configuration.Category;
+
+public sealed class RangeFilterRow : VerticalListNode
+{
+ private readonly CheckboxNode _enabledCheckbox;
+ private readonly NumericInputNode _minNode;
+ private readonly NumericInputNode _maxNode;
+
+ public Action? OnFilterChanged { get; set; }
+
+ public required string Label
+ {
+ get => _enabledCheckbox.String.Replace(" Filter", "");
+ init => _enabledCheckbox.String = $"{value} Filter";
+ }
+
+ public int MinBound
+ {
+ get => _minNode.Min;
+ init
+ {
+ _minNode.Min = value;
+ _maxNode.Min = value;
+ }
+ }
+
+ public int MaxBound
+ {
+ get => _minNode.Max;
+ init
+ {
+ _minNode.Max = value;
+ _maxNode.Max = value;
+ }
+ }
+
+ public RangeFilterRow()
+ {
+ FitContents = true;
+ ItemSpacing = 2.0f;
+
+ _enabledCheckbox = new CheckboxNode
+ {
+ Size = new Vector2(200, 20),
+ OnClick = isChecked =>
+ {
+ if (_minNode == null || _maxNode == null) return;
+ _minNode.IsEnabled = isChecked;
+ _maxNode.IsEnabled = isChecked;
+ OnFilterChanged?.Invoke(isChecked, _minNode.Value, _maxNode.Value);
+ },
+ };
+ AddNode(_enabledCheckbox);
+
+ var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
+
+ rangeRow.AddNode(new LabelTextNode
+ {
+ TextFlags = TextFlags.AutoAdjustNodeSize,
+ Size = new Vector2(30, 28),
+ String = "Min:",
+ });
+
+ _minNode = new NumericInputNode
+ {
+ Size = new Vector2(100, 28),
+ OnValueUpdate = val =>
+ {
+ if (_maxNode != null) OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, val, _maxNode.Value);
+ },
+ };
+ rangeRow.AddNode(_minNode);
+
+ rangeRow.AddNode(new LabelTextNode
+ {
+ TextFlags = TextFlags.AutoAdjustNodeSize,
+ Size = new Vector2(30, 28),
+ String = "Max:",
+ });
+
+ _maxNode = new NumericInputNode
+ {
+ Size = new Vector2(100, 28),
+ OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, _minNode.Value, val),
+ };
+ rangeRow.AddNode(_maxNode);
+
+ AddNode(rangeRow);
+ }
+
+ public void SetFilter(RangeFilter filter)
+ {
+ _enabledCheckbox.IsChecked = filter.Enabled;
+ _minNode.Value = filter.Min;
+ _maxNode.Value = filter.Max;
+ _minNode.IsEnabled = filter.Enabled;
+ _maxNode.IsEnabled = filter.Enabled;
+ }
+}
+
+public sealed class RangeFilterRowUint : VerticalListNode
+{
+ private readonly CheckboxNode _enabledCheckbox;
+ private readonly NumericInputNode _minNode;
+ private readonly NumericInputNode _maxNode;
+ private int _maxBound = int.MaxValue;
+
+ public Action? OnFilterChanged { get; set; }
+
+ public required string Label
+ {
+ get => _enabledCheckbox.String.Replace(" Filter", "");
+ init => _enabledCheckbox.String = $"{value} Filter";
+ }
+
+ public int MinBound
+ {
+ get => _minNode.Min;
+ init
+ {
+ _minNode.Min = value;
+ _maxNode.Min = value;
+ }
+ }
+
+ public int MaxBound
+ {
+ get => _maxBound;
+ init
+ {
+ _maxBound = value;
+ _minNode.Max = value;
+ _maxNode.Max = value;
+ }
+ }
+
+ public RangeFilterRowUint()
+ {
+ FitContents = true;
+ ItemSpacing = 2.0f;
+
+ _enabledCheckbox = new CheckboxNode
+ {
+ Size = new Vector2(200, 20),
+ OnClick = isChecked =>
+ {
+ if (_minNode == null || _maxNode == null) return;
+ _minNode.IsEnabled = isChecked;
+ _maxNode.IsEnabled = isChecked;
+ OnFilterChanged?.Invoke(isChecked, (uint)_minNode.Value, (uint)_maxNode.Value);
+ },
+ };
+ AddNode(_enabledCheckbox);
+
+ var rangeRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 8.0f };
+
+ rangeRow.AddNode(new LabelTextNode
+ {
+ TextFlags = TextFlags.AutoAdjustNodeSize,
+ Size = new Vector2(30, 28),
+ String = "Min:",
+ });
+
+ _minNode = new NumericInputNode
+ {
+ Size = new Vector2(100, 28),
+ OnValueUpdate = val =>
+ {
+ if (_maxNode != null)
+ OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)val, (uint)_maxNode.Value);
+ },
+ };
+ rangeRow.AddNode(_minNode);
+
+ rangeRow.AddNode(new LabelTextNode
+ {
+ TextFlags = TextFlags.AutoAdjustNodeSize,
+ Size = new Vector2(30, 28),
+ String = "Max:",
+ });
+
+ _maxNode = new NumericInputNode
+ {
+ Size = new Vector2(100, 28),
+ OnValueUpdate = val => OnFilterChanged?.Invoke(_enabledCheckbox.IsChecked, (uint)_minNode.Value, (uint)val),
+ };
+ rangeRow.AddNode(_maxNode);
+
+ AddNode(rangeRow);
+ }
+
+ public void SetFilter(RangeFilter filter)
+ {
+ _enabledCheckbox.IsChecked = filter.Enabled;
+ _minNode.Value = (int)filter.Min;
+ _maxNode.Value = (int)Math.Min(filter.Max, _maxBound);
+ _minNode.IsEnabled = filter.Enabled;
+ _maxNode.IsEnabled = filter.Enabled;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs
index c100034..1dc10b4 100644
--- a/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/RarityEditorNode.cs
@@ -1,25 +1,33 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit.Classes;
-using KamiToolKit.Nodes;
using System;
using System.Collections.Generic;
using System.Numerics;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
-public sealed class RarityEditorNode : VerticalListNode
+public sealed class RarityEditorNode :VerticalListNode
{
- private static readonly string[] RarityNames = { "Common (White)", "Uncommon (Green)", "Rare (Blue)", "Relic (Purple)", "Aetherial (Pink)" };
+ private const float LabelWidth = 120f;
+ private const float CheckboxWidth = 150f;
- private List _list;
- private readonly List _checkboxes = new();
- private readonly Action? _onChanged;
+ private static readonly string[] RarityNames =
+ [
+ "Common (White)",
+ "Uncommon (Green)",
+ "Rare (Blue)",
+ "Relic (Purple)",
+ "Aetherial (Pink)"
+ ];
- public RarityEditorNode(List list, Action? onChanged = null)
+ public Action? OnChanged { get; set; }
+
+ private List _list = [];
+ private readonly List _checkboxes = [];
+
+ public RarityEditorNode()
{
- _list = list;
- _onChanged = onChanged;
-
FitContents = true;
ItemSpacing = 2.0f;
@@ -32,33 +40,35 @@ public sealed class RarityEditorNode : VerticalListNode
};
AddNode(headerLabel);
- for (int i = 0; i < RarityNames.Length; i++)
+ for (var i = 0; i < RarityNames.Length; i++)
{
var rarity = i;
var checkbox = new CheckboxNode
{
- Size = new Vector2(200, 20),
+ Size = new Vector2(LabelWidth + CheckboxWidth, 22),
String = RarityNames[i],
- IsChecked = _list.Contains(i),
- OnClick = isChecked =>
- {
- if (isChecked && !_list.Contains(rarity))
- {
- _list.Add(rarity);
- _list.Sort();
- }
- else if (!isChecked && _list.Contains(rarity))
- {
- _list.Remove(rarity);
- }
- _onChanged?.Invoke();
- },
+ OnClick = isChecked => ToggleRarity(rarity, isChecked),
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
}
+ private void ToggleRarity(int rarity, bool isChecked)
+ {
+ if (isChecked && !_list.Contains(rarity))
+ {
+ _list.Add(rarity);
+ _list.Sort();
+ }
+ else if (!isChecked && _list.Contains(rarity))
+ {
+ _list.Remove(rarity);
+ }
+
+ OnChanged?.Invoke();
+ }
+
public void SetList(List newList)
{
_list = newList;
@@ -67,7 +77,7 @@ public sealed class RarityEditorNode : VerticalListNode
public void Refresh()
{
- for (int i = 0; i < _checkboxes.Count; i++)
+ for (var i = 0; i < _checkboxes.Count; i++)
{
_checkboxes[i].IsChecked = _list.Contains(i);
}
diff --git a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs
index d11d251..707a708 100644
--- a/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/StateFilterRowNode.cs
@@ -2,6 +2,7 @@ using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
+using KamiToolKit.Premade.Nodes;
using System;
using System.Numerics;
@@ -9,48 +10,54 @@ namespace AetherBags.Nodes.Configuration.Category;
public sealed class StateFilterRowNode : HorizontalListNode
{
- private readonly LabelTextNode _labelNode;
- private readonly TextButtonNode _stateButton;
+ private const float LabelWidth = 120f;
+ private const float ButtonWidth = 100f;
+
+ private readonly StateFilterButton _stateButton;
private readonly Action? _onChanged;
private StateFilter _filter;
- private static readonly string[] StateLabels = { "Ignored", "Allow", "Disallow" };
-
- public StateFilterRowNode(string label, StateFilter filter, Action? onChanged = null)
+ public StateFilterRowNode(string label, StateFilter filter, Action?onChanged = null)
{
_filter = filter;
_onChanged = onChanged;
- Size = new Vector2(280, 24);
+ Size = new Vector2(LabelWidth + ButtonWidth + 8f, 24);
ItemSpacing = 8.0f;
- _labelNode = new LabelTextNode
+ var labelNode = new LabelTextNode
{
- TextFlags = TextFlags.AutoAdjustNodeSize,
- Size = new Vector2(100, 24),
+ Size = new Vector2(LabelWidth, 24),
String = $"{label}:",
TextColor = ColorHelper.GetColor(8),
+ AlignmentType = AlignmentType.Right,
};
- AddNode(_labelNode);
+ AddNode(labelNode);
- _stateButton = new TextButtonNode
+ _stateButton = new StateFilterButton
{
- Size = new Vector2(100, 24),
- String = StateLabels[_filter.State],
- OnClick = CycleState,
+ Size = new Vector2(ButtonWidth, 24),
+ States = [0, 1, 2],
+ SelectedState = _filter.State,
+ OnStateChanged = newState =>
+ {
+ _filter.State = newState;
+ _onChanged?.Invoke();
+ }
};
AddNode(_stateButton);
}
- private void CycleState()
- {
- _filter.State = (_filter.State + 1) % 3;
- _stateButton.String = StateLabels[_filter.State];
- _onChanged?.Invoke();
- }
-
public void SetState(StateFilter newFilter)
{
_filter = newFilter;
- _stateButton.String = StateLabels[_filter.State];
+ _stateButton.SelectedState = _filter.State;
+ }
+
+ private sealed class StateFilterButton : MultiStateButtonNode
+ {
+ private static readonly string[] StateLabels = ["Ignored", "Required", "Excluded"];
+
+ protected override string GetStateText(int state)
+ => state >= 0 && state < StateLabels.Length ?StateLabels[state] : "Unknown";
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs
index cc13f98..5cbcd65 100644
--- a/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/StringListEditorNode.cs
@@ -1,84 +1,77 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit.Classes;
-using KamiToolKit.Nodes;
using System;
using System.Collections.Generic;
using System.Numerics;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class StringListEditorNode : VerticalListNode
{
- private List _list;
- private readonly TextInputNode _addInput;
+ private const float LabelWidth = 300f;
+ private const float RowHeight = 28f;
+
+ private List _list = [];
+
+ private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer;
- private readonly Action? _onChanged;
+ private readonly HorizontalListNode _addRow;
+ private readonly TextInputNode _addInput;
- public StringListEditorNode(string label, List list, Action? onChanged = null)
+ public Action? OnChanged { get; set; }
+
+ public required string Label
{
- _list = list;
- _onChanged = onChanged;
+ get => _headerLabel.String;
+ init => _headerLabel.String = value;
+ }
+ public StringListEditorNode()
+ {
FitContents = true;
ItemSpacing = 4.0f;
- var headerLabel = new LabelTextNode
+ _headerLabel = new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18),
- String = label,
TextColor = ColorHelper.GetColor(8),
};
- AddNode(headerLabel);
+ AddNode(_headerLabel);
_itemsContainer = new VerticalListNode
{
- FitContents = true,
+ Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f,
+ FitContents = true,
+ FirstItemSpacing = 2,
};
AddNode(_itemsContainer);
- var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f };
+ _addRow = new HorizontalListNode
+ {
+ Size = new Vector2(LabelWidth + 40f, RowHeight),
+ ItemSpacing = 4.0f,
+ };
_addInput = new TextInputNode
{
- Size = new Vector2(200, 28),
+ Size = new Vector2(200, RowHeight),
PlaceholderString = "Add new...",
- OnInputComplete = text =>
- {
- var value = text.ExtractText();
- if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value))
- {
- _list.Add(value);
- _addInput?.String = "";
- RefreshItems();
- _onChanged?.Invoke();
- }
- },
+ OnInputComplete = _ => AddCurrentValue(),
};
- addRow.AddNode(_addInput);
+ _addRow.AddNode(_addInput);
var addButton = new TextButtonNode
{
- Size = new Vector2(60, 28),
+ Size = new Vector2(60, RowHeight),
String = "Add",
- OnClick = () =>
- {
- var value = _addInput.String;
- if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
- {
- _list.Add(value);
- _addInput.String = "";
- RefreshItems();
- _onChanged?.Invoke();
- }
- },
+ OnClick = AddCurrentValue,
};
- addRow.AddNode(addButton);
+ _addRow.AddNode(addButton);
- AddNode(addRow);
-
- RefreshItems();
+ AddNode(_addRow);
}
public void SetList(List newList)
@@ -87,35 +80,54 @@ public sealed class StringListEditorNode : VerticalListNode
RefreshItems();
}
+ private void AddCurrentValue()
+ {
+ var value = _addInput.String;
+ if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
+ {
+ _list.Add(value);
+ _addInput.String = "";
+ RefreshItems();
+ OnChanged?.Invoke();
+ }
+ }
+
private void RefreshItems()
{
- _itemsContainer.SyncWithListData(
- _list,
- node => node.Value,
- value => new StringListItemNode(value)
- {
- Size = new Vector2(280, 22),
- OnRemove = () =>
- {
- _list.Remove(value);
- RefreshItems();
- _onChanged?.Invoke();
- },
- }
- );
+ _itemsContainer.Clear();
+
+ foreach (var value in _list)
+ {
+ _itemsContainer.AddNode(CreateItemNode(value));
+ }
+
+ if (_list.Count == 0)
+ {
+ _itemsContainer.Height = 0;
+ }
_itemsContainer.RecalculateLayout();
RecalculateLayout();
}
- public void Refresh()
+ private StringListItemNode CreateItemNode(string value) => new(value)
{
+ Size = new Vector2(LabelWidth + 40f, RowHeight),
+ OnRemove = () => RemoveValue(value),
+ };
+
+ private void RemoveValue(string value)
+ {
+ _list.Remove(value);
RefreshItems();
+ OnChanged?.Invoke();
}
}
public sealed class StringListItemNode : HorizontalListNode
{
+ private const float LabelWidth = 300f;
+
public string Value { get; }
public Action? OnRemove { get; init; }
@@ -124,20 +136,18 @@ public sealed class StringListItemNode : HorizontalListNode
Value = value;
ItemSpacing = 4.0f;
- var itemLabel = new LabelTextNode
+ AddNode(new LabelTextNode
{
- Size = new Vector2(220, 22),
+ Size = new Vector2(LabelWidth, 24),
String = value,
TextColor = ColorHelper.GetColor(3),
- };
- AddNode(itemLabel);
+ });
- var removeButton = new TextButtonNode
+ AddNode(new CircleButtonNode
{
- Size = new Vector2(50, 22),
- String = "X",
+ Size = new Vector2(28, 28),
+ Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(),
- };
- AddNode(removeButton);
+ });
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs
index 93dbef1..458ab75 100644
--- a/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs
+++ b/AetherBags/Nodes/Configuration/Category/UintListEditorNode.cs
@@ -1,76 +1,79 @@
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit.Classes;
-using KamiToolKit.Nodes;
using System;
using System.Collections.Generic;
using System.Numerics;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public sealed class UintListEditorNode : VerticalListNode
{
- private List _list;
- private readonly NumericInputNode _addInput;
+ private const float LabelWidth = 300f;
+ private const float RowHeight = 28f;
+
+ private List _list = [];
+
+ private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer;
- private readonly Action? _onChanged;
- private readonly Func? _labelResolver;
+ private readonly HorizontalListNode _addRow;
+ private readonly NumericInputNode _addInput;
- public UintListEditorNode(string label, List list, Action? onChanged = null, Func? labelResolver = null)
+ public Func? LabelResolver { get; init; }
+ public Action? OnChanged { get; set; }
+
+ public required string Label
{
- _list = list;
- _onChanged = onChanged;
- _labelResolver = labelResolver;
+ get => _headerLabel.String;
+ init => _headerLabel.String = value;
+ }
+ public UintListEditorNode()
+ {
FitContents = true;
ItemSpacing = 4.0f;
- var headerLabel = new LabelTextNode
+ _headerLabel = new LabelTextNode
{
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18),
- String = label,
TextColor = ColorHelper.GetColor(8),
};
- AddNode(headerLabel);
+ AddNode(_headerLabel);
_itemsContainer = new VerticalListNode
{
- FitContents = true,
+ Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f,
+ FitContents = true,
+ FirstItemSpacing = 2,
};
AddNode(_itemsContainer);
- var addRow = new HorizontalListNode { Size = new Vector2(300, 28), ItemSpacing = 4.0f };
+ _addRow = new HorizontalListNode
+ {
+ Size = new Vector2(LabelWidth + 40f, RowHeight),
+ ItemSpacing = 4.0f,
+ };
_addInput = new NumericInputNode
{
- Size = new Vector2(120, 28),
+ Size = new Vector2(120, RowHeight),
Min = 0,
Max = int.MaxValue,
Value = 0,
};
- addRow.AddNode(_addInput);
+ _addRow.AddNode(_addInput);
var addButton = new TextButtonNode
{
- Size = new Vector2(60, 28),
+ Size = new Vector2(60, RowHeight),
String = "Add",
- OnClick = () =>
- {
- var value = (uint)_addInput.Value;
- if (! _list.Contains(value))
- {
- _list.Add(value);
- RefreshItems();
- _onChanged?.Invoke();
- }
- },
+ OnClick = AddCurrentValue,
};
- addRow.AddNode(addButton);
+ _addRow.AddNode(addButton);
- AddNode(addRow);
-
- RefreshItems();
+ AddNode(_addRow);
}
public void SetList(List newList)
@@ -79,35 +82,53 @@ public sealed class UintListEditorNode : VerticalListNode
RefreshItems();
}
+ private void AddCurrentValue()
+ {
+ var value = (uint)_addInput.Value;
+ if (!_list.Contains(value))
+ {
+ _list.Add(value);
+ RefreshItems();
+ OnChanged?.Invoke();
+ }
+ }
+
private void RefreshItems()
{
- _itemsContainer.SyncWithListData(
- _list,
- node => node.Value,
- value => new UintListItemNode(value, _labelResolver)
- {
- Size = new Vector2(280, 22),
- OnRemove = () =>
- {
- _list.Remove(value);
- RefreshItems();
- _onChanged?.Invoke();
- },
- }
- );
+ _itemsContainer.Clear();
+
+ foreach (var value in _list)
+ {
+ _itemsContainer.AddNode(CreateItemNode(value));
+ }
+
+ if (_list.Count == 0)
+ {
+ _itemsContainer.Height = 0;
+ }
_itemsContainer.RecalculateLayout();
RecalculateLayout();
}
- public void Refresh()
+ private UintListItemNode CreateItemNode(uint value) => new(value, LabelResolver)
{
+ Size = new Vector2(LabelWidth + 40f, RowHeight),
+ OnRemove = () => RemoveValue(value),
+ };
+
+ private void RemoveValue(uint value)
+ {
+ _list.Remove(value);
RefreshItems();
+ OnChanged?.Invoke();
}
}
-public sealed class UintListItemNode : HorizontalListNode
+public sealed class UintListItemNode : HorizontalListNode
{
+ private const float LabelWidth = 300f;
+
public uint Value { get; }
public Action? OnRemove { get; init; }
@@ -116,22 +137,22 @@ public sealed class UintListItemNode : HorizontalListNode
Value = value;
ItemSpacing = 4.0f;
- var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString();
- var itemLabel = new LabelTextNode
+ var displayText = labelResolver is not null
+ ? $"{value} - {labelResolver(value)}"
+ : value.ToString();
+
+ AddNode(new LabelTextNode
{
- TextFlags = TextFlags.AutoAdjustNodeSize,
- Size = new Vector2(220, 22),
+ Size = new Vector2(LabelWidth, 24),
String = displayText,
TextColor = ColorHelper.GetColor(3),
- };
- AddNode(itemLabel);
+ });
- var removeButton = new TextButtonNode
+ AddNode(new CircleButtonNode
{
- Size = new Vector2(50, 22),
- String = "X",
+ Size = new Vector2(28, 28),
+ Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(),
- };
- AddNode(removeButton);
+ });
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/ConfigurationRoot.cs b/AetherBags/Nodes/Configuration/ConfigurationRoot.cs
deleted file mode 100644
index 5d9b617..0000000
--- a/AetherBags/Nodes/Configuration/ConfigurationRoot.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-using KamiToolKit.Nodes;
-
-namespace AetherBags.Nodes.Configuration;
-
-internal class ConfigurationRoot : TabbedVerticalListNode
-{
-}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs
index f8c1006..e709455 100644
--- a/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs
+++ b/AetherBags/Nodes/Configuration/Currency/CurrencyGeneralConfigurationNode.cs
@@ -12,6 +12,8 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
{
CurrencySettings config = System.Config.Currency;
+ ItemVerticalSpacing = 2;
+
LabelTextNode titleNode = new LabelTextNode
{
Size = Size with { Y = 18 },
@@ -51,14 +53,13 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
};
AddNode(defaultCurrencyColorNode);
- AddNode();
-
CheckboxNode cappedEnabledCheckbox = new CheckboxNode
{
Size = Size with { Y = 18 },
IsVisible = true,
- String = "Color When Capped",
+ String = "Color Weekly Cap",
IsChecked = config.ColorWhenCapped,
+ TextTooltip = "Changes the color of the currency display when you have reached the maximum amount earnable for the current week (e.g., 450/450).",
OnClick = isChecked =>
{
config.ColorWhenCapped = isChecked;
@@ -69,9 +70,10 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
AddTab(1);
+
ColorInputRow cappedCurrencyColorNode = new ColorInputRow
{
- Label = "Capped Currency Color",
+ Label = "Weekly Cap Color",
Size = new Vector2(300, 24),
CurrentColor = config.CappedColor,
DefaultColor = new CurrencySettings().CappedColor,
@@ -89,8 +91,9 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
{
Size = Size with { Y = 18 },
IsVisible = true,
- String = "Color Weekly Limit",
+ String = "Color Max Capacity",
IsChecked = config.ColorWhenLimited,
+ TextTooltip = "Changes the color of the currency display when your total held amount has reached its maximum capacity (e.g., 2000/2000).",
OnClick = isChecked =>
{
config.ColorWhenLimited = isChecked;
@@ -103,7 +106,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
ColorInputRow limitCurrencyColorNode = new ColorInputRow
{
- Label = "Limit Currency Color",
+ Label = "Max Capacity Color",
Size = new Vector2(300, 24),
CurrentColor = config.LimitColor,
DefaultColor = new CurrencySettings().LimitColor,
diff --git a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs
index f0e074f..e8188e0 100644
--- a/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs
+++ b/AetherBags/Nodes/Configuration/Currency/CurrencyScrollingAreaNode.cs
@@ -2,11 +2,11 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Currency;
-public sealed class CurrencyScrollingAreaNode : ScrollingAreaNode
+public sealed class CurrencyScrollingAreaNode : ScrollingListNode
{
public CurrencyScrollingAreaNode()
{
- ContentNode.AddNode(new CurrencyGeneralConfigurationNode
+ AddNode(new CurrencyGeneralConfigurationNode
{
Size = Size
});
diff --git a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs
index 19fc24f..825af76 100644
--- a/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs
+++ b/AetherBags/Nodes/Configuration/General/FunctionalConfigurationNode.cs
@@ -2,6 +2,8 @@ using System;
using System.Linq;
using System.Numerics;
using AetherBags.Configuration;
+using AetherBags.Inventory;
+using AetherBags.Nodes.Input;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
@@ -10,12 +12,16 @@ namespace AetherBags.Nodes.Configuration.General;
internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
{
private readonly CheckboxNode _hideDefaultBagsCheckboxNode;
+ private readonly CheckboxNode _hideSaddlebagsCheckboxNode;
+ private readonly CheckboxNode _hideRetainerbagsCheckboxNode;
private readonly LabeledDropdownNode _stackDropDown;
public FunctionalConfigurationNode()
{
GeneralSettings config = System.Config.General;
+ ItemVerticalSpacing = 2;
+
var titleNode = new CategoryTextNode
{
Height = 18,
@@ -55,6 +61,66 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
AddNode(_hideDefaultBagsCheckboxNode);
SubtractTab(1);
+ var showSaddleWithGameCheckBox = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Auto-open Saddlebags with game Saddlebags",
+ IsChecked = config.OpenSaddleBagsWithGameInventory,
+ OnClick = isChecked =>
+ {
+ config.OpenSaddleBagsWithGameInventory = isChecked;
+ _hideSaddlebagsCheckboxNode?.IsEnabled = isChecked;
+ }
+ };
+ AddNode(showSaddleWithGameCheckBox);
+
+ AddTab(1);
+ _hideSaddlebagsCheckboxNode = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Hide default Saddlebags",
+ IsEnabled = config.OpenSaddleBagsWithGameInventory,
+ IsChecked = config.HideGameSaddleBags,
+ OnClick = isChecked =>
+ {
+ config.HideGameSaddleBags = isChecked;
+ }
+ };
+ AddNode(_hideSaddlebagsCheckboxNode);
+ SubtractTab(1);
+
+ var showRetainerWithGameCheckBox = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Auto-open Retainer bags with game Retainer bags",
+ IsChecked = config.OpenRetainerWithGameInventory,
+ OnClick = isChecked =>
+ {
+ config.OpenRetainerWithGameInventory = isChecked;
+ _hideRetainerbagsCheckboxNode?.IsEnabled = isChecked;
+ }
+ };
+ AddNode(showRetainerWithGameCheckBox);
+
+ AddTab(1);
+ _hideRetainerbagsCheckboxNode = new CheckboxNode
+ {
+ Size = Size with { Y = 18 },
+ IsVisible = true,
+ String = "Hide default Retainer bags",
+ IsEnabled = config.OpenRetainerWithGameInventory,
+ IsChecked = config.HideGameRetainer,
+ OnClick = isChecked =>
+ {
+ config.HideGameRetainer = isChecked;
+ }
+ };
+ AddNode(_hideRetainerbagsCheckboxNode);
+ SubtractTab(1);
+
var linkItemCheckBox = new CheckboxNode
{
Size = Size with { Y = 18 },
@@ -68,6 +134,29 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
};
AddNode(linkItemCheckBox);
+ AddNode(new ResNode
+ {
+ Height = 6
+ });
+
+ var searchModeDropDown = new LabeledDropdownNode
+ {
+ Size = new Vector2(300, 20),
+ LabelText = "Search Mode",
+ LabelTextFlags = TextFlags.AutoAdjustNodeSize,
+ Options = Enum.GetNames(typeof(SearchMode)).ToList(),
+ SelectedOption = config.SearchMode.ToString(),
+ OnOptionSelected = selected =>
+ {
+ if (Enum.TryParse(selected, out var parsed))
+ {
+ config.SearchMode = parsed;
+ InventoryOrchestrator.RefreshAll(updateMaps: false);
+ }
+ }
+ };
+ AddNode(searchModeDropDown);
+
_stackDropDown = new LabeledDropdownNode
{
Size = new Vector2(300, 20),
@@ -81,7 +170,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
if (Enum.TryParse(selected, out var parsed))
{
config.StackMode = parsed;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
}
};
diff --git a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs
index 253f864..f9d4e53 100644
--- a/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs
+++ b/AetherBags/Nodes/Configuration/General/GeneralScrollingAreaNode.cs
@@ -5,30 +5,30 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.General;
-public sealed class GeneralScrollingAreaNode : ScrollingAreaNode
+public sealed class GeneralScrollingAreaNode : ScrollingListNode
{
- private readonly CheckboxNode _debugCheckboxNode = null!;
-
public GeneralScrollingAreaNode()
{
GeneralSettings config = System.Config.General;
- ContentNode.ItemSpacing = 32;
+ new ImportExportResetNode().AttachNode(this);
- ContentNode.AddNode(new FunctionalConfigurationNode());
+ ItemSpacing = 10;
- ContentNode.AddNode(new LayoutConfigurationNode());
+ AddNode(new FunctionalConfigurationNode());
- _debugCheckboxNode = new CheckboxNode
+ AddNode(new LayoutConfigurationNode());
+
+ AddNode(new CheckboxNode
{
Size = new Vector2(300, 20),
IsVisible = true,
String = "Debug Mode",
IsChecked = config.DebugEnabled,
- OnClick = isChecked => { config.DebugEnabled = isChecked; }
- };
- ContentNode.AddNode(_debugCheckboxNode);
+ OnClick = isChecked =>
+ {
+ config.DebugEnabled = isChecked;
+ }
+ });
}
-
- private void RefreshInventory() => System.AddonInventoryWindow.ManualInventoryRefresh();
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs
new file mode 100644
index 0000000..a8a82fb
--- /dev/null
+++ b/AetherBags/Nodes/Configuration/General/ImportExportResetNode.cs
@@ -0,0 +1,71 @@
+using System;
+using System.IO;
+using AetherBags.Helpers;
+using AetherBags.Inventory;
+using Dalamud.Game.ClientState.Keys;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes.Configuration.General;
+
+public sealed class ImportExportResetNode : HorizontalListNode
+{
+ public ImportExportResetNode()
+ {
+ Height = 0;
+ Width = 600;
+ Alignment = HorizontalListAnchor.Right;
+ FirstItemSpacing = 3;
+ ItemSpacing = 2;
+ IsVisible = true;
+
+ AddNode(new ImGuiIconButtonNode {
+ Y = 3,
+ Height = 30,
+ Width = 30,
+ IsVisible = true,
+ TextTooltip = " Import Configuration\n(hold shift to confirm)",
+ TexturePath = Path.Combine(Services.PluginInterface.AssemblyLocation.Directory?.FullName!, @"Assets\Icons\download.png"),
+ OnClick = ImportConfig
+ });
+
+ AddNode(new ImGuiIconButtonNode {
+ Y = 3,
+ Height = 30,
+ Width = 30,
+ IsVisible = true,
+ TextTooltip = "Export Configuration",
+ TexturePath = Path.Combine(Services.PluginInterface.AssemblyLocation.Directory?.FullName!, @"Assets\Icons\upload.png"),
+ OnClick = ExportConfig
+ });
+
+ AddNode(new HoldButtonNode {
+ IsVisible = true,
+ Y = 0,
+ Height = 32,
+ Width = 100,
+ String = "Reset",
+ TextNode = { TextColor = ColorHelper.GetColor(50) },
+ TextTooltip = " Reset configuration\n(hold button to confirm)",
+ OnClick = ResetConfig
+ });
+ }
+
+ private static void ResetConfig()
+ {
+ InventoryOrchestrator.CloseAll();
+ ImportExportResetHelper.TryResetConfig();
+ System.AddonConfigurationWindow.Close();
+ }
+
+ private static void ImportConfig()
+ {
+ if (!Services.KeyState[VirtualKey.SHIFT]) return;
+
+ InventoryOrchestrator.CloseAll();
+ ImportExportResetHelper.TryImportConfigFromClipboard();
+ System.AddonConfigurationWindow.Close();
+ }
+
+ private static void ExportConfig() => ImportExportResetHelper.TryExportConfigToClipboard(System.Config);
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs
index b01c560..5c2e597 100644
--- a/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs
+++ b/AetherBags/Nodes/Configuration/Layout/CompactLookaheadNode.cs
@@ -2,6 +2,7 @@
using KamiToolKit.Classes.Timelines;
using KamiToolKit.Nodes;
using System.Numerics;
+using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Component.GUI;
namespace AetherBags.Nodes.Configuration.Layout;
@@ -33,7 +34,7 @@ internal sealed class CompactLookaheadNode : SimpleComponentNode
OnValueUpdate = value =>
{
config.CompactLookahead = value;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
};
CompactLookahead.AttachNode(this);
diff --git a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs
index 93db895..c68954c 100644
--- a/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs
+++ b/AetherBags/Nodes/Configuration/Layout/LayoutConfigurationNode.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using AetherBags.Configuration;
+using AetherBags.Inventory;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Layout;
@@ -32,7 +33,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked =>
{
config.ShowCategoryItemCount = isChecked;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
};
AddNode(showCategoryItemAmountCheckboxNode);
@@ -49,7 +50,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
_preferLargestFitCheckboxNode.IsEnabled = isChecked;
_useStableInsertCheckboxNode.IsEnabled = isChecked;
_compactLookaheadNode.CompactLookahead.IsEnabled = isChecked;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
};
AddNode(compactPackingCheckboxNode);
@@ -65,7 +66,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked =>
{
config.CompactPreferLargestFit = isChecked;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
};
AddNode(_preferLargestFitCheckboxNode);
@@ -80,7 +81,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked =>
{
config.CompactStableInsert = isChecked;
- System.AddonInventoryWindow.ManualInventoryRefresh();
+ InventoryOrchestrator.RefreshAll(updateMaps: true);
}
};
AddNode(_useStableInsertCheckboxNode);
diff --git a/AetherBags/Nodes/Currency/CurrencyNode.cs b/AetherBags/Nodes/Currency/CurrencyNode.cs
index a3adf98..25b5c6e 100644
--- a/AetherBags/Nodes/Currency/CurrencyNode.cs
+++ b/AetherBags/Nodes/Currency/CurrencyNode.cs
@@ -51,7 +51,7 @@ public class CurrencyNode : SimpleComponentNode
_countNode.TextColor =
isLimited ? config.LimitColor :
- isCapped ? config.CappedColor :
+ isCapped ? config.CappedColor :
config.DefaultColor;
}
}
diff --git a/AetherBags/Nodes/DragDropNode.cs b/AetherBags/Nodes/DragDropNode.cs
deleted file mode 100644
index f99f3b2..0000000
--- a/AetherBags/Nodes/DragDropNode.cs
+++ /dev/null
@@ -1,242 +0,0 @@
-using System;
-using System.Numerics;
-using AetherBags.Interop;
-using FFXIVClientStructs.FFXIV.Client.Enums;
-using FFXIVClientStructs.FFXIV.Client.UI;
-using FFXIVClientStructs.FFXIV.Client.UI.Agent;
-using FFXIVClientStructs.FFXIV.Component.GUI;
-using KamiToolKit.Classes;
-using KamiToolKit.Classes.Timelines;
-using KamiToolKit.Nodes;
-
-namespace AetherBags.Nodes;
-
-public unsafe class DragDropNode : ComponentNode {
-
- // FIX: Manually expose the pointers that are 'internal' in KamiToolKit
- // We access the raw AtkComponentNode* via 'this.ResNode' and cast from there.
- private AtkComponentDragDrop* Component => (AtkComponentDragDrop*)Node->Component;
- private AtkUldComponentDataDragDrop* Data => (AtkUldComponentDataDragDrop*)Component->UldManager.ComponentData;
-
- public readonly ImageNode DragDropBackgroundNode;
- public readonly IconNode IconNode;
-
- public DragDropNode() {
- SetInternalComponentType(ComponentType.DragDrop);
-
- DragDropBackgroundNode = new SimpleImageNode {
- NodeId = 3,
- Size = new Vector2(44.0f, 44.0f),
- TexturePath = "ui/uld/DragTargetA.tex",
- TextureCoordinates = new Vector2(0.0f, 0.0f),
- TextureSize = new Vector2(44.0f, 44.0f),
- WrapMode = WrapMode.Tile,
- NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
- };
- DragDropBackgroundNode.AttachNode(this);
-
- IconNode = new IconNode {
- NodeId = 2,
- Size = new Vector2(44.0f, 48.0f),
- NodeFlags = NodeFlags.Visible | NodeFlags.Enabled | NodeFlags.EmitsEvents,
- };
- IconNode.AttachNode(this);
-
- LoadTimelines();
-
- Data->Nodes[0] = IconNode.NodeId;
-
- AcceptedType = DragDropType.Everything;
- Payload = new DragDropPayload();
-
- // Use the fixed shadow struct for writing initial values if needed,
- // though direct field access on the struct usually works for simple fields.
- // However, to be safe with the VTable fix, we just set fields directly here
- // as they are standard offsets, or use the pointer.
- Component->AtkDragDropInterface.DragDropType = DragDropType.Everything;
- Component->AtkDragDropInterface.DragDropReferenceIndex = 0;
-
- InitializeComponentEvents();
-
- AddEvent(AtkEventType.DragDropBegin, DragDropBeginHandler);
- AddEvent(AtkEventType.DragDropInsert, DragDropInsertHandler);
- AddEvent(AtkEventType.DragDropDiscard, DragDropDiscardHandler);
- AddEvent(AtkEventType.DragDropClick, DragDropClickHandler);
- AddEvent(AtkEventType.DragDropRollOver, DragDropRollOverHandler);
- AddEvent(AtkEventType.DragDropRollOut, DragDropRollOutHandler);
- }
-
- private bool IsDragDropEndRegistered { get; set; }
-
- public Action? OnBegin { get; set; }
- public Action? OnEnd { get; set; }
- public Action? OnPayloadAccepted { get; set; }
- public Action? OnDiscard { get; set; }
- public Action? OnClicked { get; set; }
- public Action? OnRollOver { get; set; }
- public Action? OnRollOut { get; set; }
-
- public DragDropPayload Payload { get; set; }
-
- public uint IconId {
- get => IconNode.IconId;
- set {
- IconNode.IconId = value;
- IconNode.IsVisible = value != 0;
- }
- }
-
- public bool IsIconDisabled {
- get => IconNode.IsIconDisabled;
- set => IconNode.IsIconDisabled = value;
- }
-
- public int Quantity {
- get => int.Parse(Component->GetQuantityText().ToString());
- set => Component->SetQuantity(value);
- }
-
- public string QuantityString {
- get => Component->GetQuantityText().ToString();
- set => Component->SetQuantityText(value);
- }
-
- public DragDropType AcceptedType {
- get => Component->AcceptedType;
- set => Component->AcceptedType = value;
- }
-
- public AtkDragDropInterface.SoundEffectSuppression SoundEffectSuppression {
- get => Component->AtkDragDropInterface.DragDropSoundEffectSuppression;
- set => Component->AtkDragDropInterface.DragDropSoundEffectSuppression = value;
- }
-
- public bool IsDraggable {
- get => !Component->Flags.HasFlag(DragDropFlag.Locked);
- set {
- if (value) {
- Component->Flags &= ~DragDropFlag.Locked;
- }
- else {
- Component->Flags |= DragDropFlag.Locked;
- }
- }
- }
-
- public bool IsClickable {
- get => Component->Flags.HasFlag(DragDropFlag.Clickable);
- set {
- if (value) {
- Component->Flags |= DragDropFlag.Clickable;
- }
- else {
- Component->Flags &= ~DragDropFlag.Clickable;
- }
- }
- }
-
- private void DragDropBeginHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
- atkEvent->SetEventIsHandled();
-
- // FIX: Use extension method to write payload using fixed VTable
- Payload.ToFixedInterface(atkEventData->DragDropData.DragDropInterface);
-
- OnBegin?.Invoke(this);
-
- if (!IsDragDropEndRegistered) {
- AddEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
- IsDragDropEndRegistered = true;
- }
- }
-
- private void DragDropInsertHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
- atkEvent->SetEventIsHandled();
-
- atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
- atkEvent->State.ReturnFlags = 1;
-
- // FIX: Use extension method to read payload using fixed VTable
- var payload = DragDropPayloadExtensions.FromFixedInterface(atkEventData->DragDropData.DragDropInterface);
-
- Payload.Clear();
- IconId = 0;
-
- OnPayloadAccepted?.Invoke(this, payload);
- }
-
- private void DragDropDiscardHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
- atkEvent->SetEventIsHandled();
-
- atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
- atkEvent->State.ReturnFlags = 1;
-
- OnDiscard?.Invoke(this);
- }
-
- private void DragDropEndHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
- atkEvent->SetEventIsHandled();
-
- // FIX: Cast to shadow struct to call the correct GetPayloadContainer (Index 12)
- var fixedInterface = (AtkDragDropInterfaceFixed*)atkEventData->DragDropData.DragDropInterface;
- fixedInterface->GetPayloadContainer()->Clear();
-
- OnEnd?.Invoke(this);
-
- if (IsDragDropEndRegistered) {
- RemoveEvent(AtkEventType.DragDropEnd, DragDropEndHandler);
- IsDragDropEndRegistered = false;
- }
- }
-
- private void DragDropClickHandler(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
- atkEvent->SetEventIsHandled();
-
- atkEvent->State.StateFlags |= AtkEventStateFlags.HasReturnFlags;
- atkEvent->State.ReturnFlags = 1;
-
- OnClicked?.Invoke(this);
- }
-
- private void DragDropRollOverHandler()
- => OnRollOver?.Invoke(this);
-
- private void DragDropRollOutHandler()
- => OnRollOut?.Invoke(this);
-
- public void Clear() {
- Payload.Clear();
- IconId = 0;
- }
-
- public void ShowTooltip(AtkTooltipManager.AtkTooltipType type, ActionKind actionKind) {
- if (AtkStage.Instance()->DragDropManager.IsDragging) return;
-
- // FIX: Explicitly use 'this.ResNode' and cast to (AtkResNode*) to avoid ambiguity with the class name
- var addon = RaptureAtkUnitManager.Instance()->GetAddonByNode((AtkResNode*)this);
- if (addon is null) return;
-
- var tooltipArgs = new AtkTooltipManager.AtkTooltipArgs();
- tooltipArgs.Ctor();
- tooltipArgs.ActionArgs.Id = Payload.Int2;
- tooltipArgs.ActionArgs.Kind = (DetailKind)actionKind;
-
- AtkStage.Instance()->TooltipManager.ShowTooltip(
- AtkTooltipManager.AtkTooltipType.Action,
- addon->Id,
- (AtkResNode*)this, // FIX: Explicit cast here as well
- &tooltipArgs);
- }
-
- private void LoadTimelines() {
- AddTimeline(new TimelineBuilder()
- .BeginFrameSet(1, 59)
- .AddLabelPair(1, 10, 1)
- .AddLabelPair(11, 19, 2)
- .AddLabelPair(20, 29, 3)
- .AddLabelPair(30, 39, 7)
- .AddLabelPair(40, 49, 6)
- .AddLabelPair(50, 59, 4)
- .EndFrameSet()
- .Build());
- }
-}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Input/LabeledDropdownNode.cs b/AetherBags/Nodes/Input/LabeledDropdownNode.cs
index 216eb58..e2149c5 100644
--- a/AetherBags/Nodes/Input/LabeledDropdownNode.cs
+++ b/AetherBags/Nodes/Input/LabeledDropdownNode.cs
@@ -3,7 +3,7 @@ using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes;
-namespace AetherBags.Nodes;
+namespace AetherBags.Nodes.Input;
public class LabeledDropdownNode : SimpleComponentNode {
private readonly GridNode _gridNode;
diff --git a/AetherBags/Nodes/Input/TextInputWithButtonNode.cs b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs
new file mode 100644
index 0000000..8b9d56f
--- /dev/null
+++ b/AetherBags/Nodes/Input/TextInputWithButtonNode.cs
@@ -0,0 +1,55 @@
+using System;
+using System.Numerics;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+using Lumina.Text.ReadOnly;
+
+namespace AetherBags.Nodes.Input;
+
+public class TextInputWithButtonNode : SimpleComponentNode {
+ private readonly TextInputNode _textInputNode;
+ private readonly CircleButtonNode _contextButton;
+
+ public Action? OnButtonClicked {
+ get => _contextButton.OnClick;
+ set => _contextButton.OnClick = value;
+ }
+
+ public TextInputWithButtonNode() {
+ _textInputNode = new TextInputNode {
+ PlaceholderString = "Search . . .",
+ };
+ _textInputNode.AttachNode(this);
+
+ _contextButton = new CircleButtonNode {
+ Icon = ButtonIcon.Filter,
+ Size = new Vector2(28f),
+ };
+ _contextButton.AttachNode(this);
+ }
+
+ public Vector3 HintAddColor {
+ get => _contextButton.AddColor;
+ set => _contextButton.AddColor = value;
+ }
+
+ public required Action? OnInputReceived {
+ get => _textInputNode.OnInputReceived;
+ set => _textInputNode.OnInputReceived = value;
+ }
+
+ protected override void OnSizeChanged() {
+ base.OnSizeChanged();
+
+ _contextButton.Size = new Vector2(Height, Height);
+ _contextButton.Position = new Vector2(Width - _contextButton.Width, 0.0f);
+
+ _textInputNode.Size = new Vector2(Width - _contextButton.Width - 5.0f, Height);
+ _textInputNode.Position = new Vector2(0.0f, 0.0f);
+ }
+
+ public ReadOnlySeString SearchString {
+ get => _textInputNode.SeString;
+ set => _textInputNode.SeString = value;
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Input/TextInputWithHintNode.cs b/AetherBags/Nodes/Input/TextInputWithHintNode.cs
deleted file mode 100644
index 8d84ff2..0000000
--- a/AetherBags/Nodes/Input/TextInputWithHintNode.cs
+++ /dev/null
@@ -1,51 +0,0 @@
-using System;
-using System.Numerics;
-using KamiToolKit.Nodes;
-using Lumina.Text;
-using Lumina.Text.ReadOnly;
-
-namespace AetherBags.Nodes.Input;
-
-public class TextInputWithHintNode : SimpleComponentNode {
- private readonly TextInputNode _textInputNode;
- private readonly ImageNode _helpNode;
-
- public TextInputWithHintNode() {
- _textInputNode = new TextInputNode {
- PlaceholderString = "Search . . .",
- };
- _textInputNode.AttachNode(this);
-
- _helpNode = new SimpleImageNode {
- TexturePath = "ui/uld/CircleButtons.tex",
- TextureCoordinates = new Vector2(112.0f, 84.0f),
- TextureSize = new Vector2(28.0f, 28.0f),
- TextTooltip = new SeStringBuilder()
- .Append("Supports Regex Search")
- .AppendNewLine()
- .Append("Start input with '$' to search by description")
- .ToReadOnlySeString(),
- };
- _helpNode.AttachNode(this);
- }
-
- public required Action? OnInputReceived {
- get => _textInputNode.OnInputReceived;
- set => _textInputNode.OnInputReceived = value;
- }
-
- protected override void OnSizeChanged() {
- base.OnSizeChanged();
-
- _helpNode.Size = new Vector2(Height, Height);
- _helpNode.Position = new Vector2(Width - _helpNode.Width - 5.0f, 0.0f);
-
- _textInputNode.Size = new Vector2(Width - _helpNode.Width - 5.0f, Height);
- _textInputNode.Position = new Vector2(0.0f, 0.0f);
- }
-
- public ReadOnlySeString SearchString {
- get => _textInputNode.SeString;
- set => _textInputNode.SeString = value;
- }
-}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
index 0d4c8d7..bf4cef5 100644
--- a/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryCategoryNode.cs
@@ -1,7 +1,10 @@
using System;
using System.Numerics;
using AetherBags.Helpers;
+using AetherBags.Hooks;
using AetherBags.Inventory;
+using AetherBags.Inventory.Categories;
+using AetherBags.Inventory.Items;
using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
@@ -11,15 +14,12 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
-// TODO: Switch back to CS version when Dalamud Updated
-using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
-
namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode
{
private readonly TextNode _categoryNameTextNode;
- private readonly HybridDirectionalFlexNode _itemGridNode;
+ private readonly HybridDirectionalFlexNode _itemGridNode;
private const float FallbackItemSize = 46;
private const float HeaderHeight = 16;
@@ -33,6 +33,8 @@ public class InventoryCategoryNode : SimpleComponentNode
private string _fullHeaderText = string.Empty;
public event Action? HeaderHoverChanged;
+ public Action? OnRefreshRequested { get; set; }
+ public Action? OnDragEnd { get; set; }
public InventoryCategoryNode()
{
@@ -51,7 +53,7 @@ public class InventoryCategoryNode : SimpleComponentNode
_categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
_categoryNameTextNode.AttachNode(this);
- _itemGridNode = new HybridDirectionalFlexNode
+ _itemGridNode = new HybridDirectionalFlexNode
{
Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 92),
@@ -218,26 +220,35 @@ public class InventoryCategoryNode : SimpleComponentNode
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{
InventoryItem item = data.Item;
- InventoryMappedLocation location = data.VisualLocation;
+ InventoryMappedLocation visualLocation = data.VisualLocation;
+
+ var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
+ int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
+
+ DragDropPayload nodePayload = new DragDropPayload
+ {
+ // Int1 is always the container ID, for Item DragDrop Int2 is only used as a fallback
+ // ReferenceIndex is the absolute index that's actually used
+ Type = DragDropType.Item,
+ Int1 = visualLocation.Container,
+ Int2 = visualLocation.Slot,
+ ReferenceIndex = (short)absoluteIndex
+ };
return new InventoryDragDropNode
{
Size = new Vector2(42, 46),
- Alpha = data.IsEligibleForContext || data.IsSlotBlocked ? 1.0f : 0.4f,
+ Alpha = data.VisualAlpha,
+ AddColor = data.HighlightOverlayColor,
+ IsDraggable = !data.IsSlotBlocked,
IsVisible = true,
IconId = item.IconId,
AcceptedType = DragDropType.Item,
- IsDraggable = !data.IsSlotBlocked,
- Payload = new DragDropPayload
- {
- Type = DragDropType.Item,
- Int1 = location.Container,
- Int2 = location.Slot,
- },
+ Payload = nodePayload,
IsClickable = true,
OnDiscard = node => OnDiscard(node, data),
- OnEnd = _ => System.AddonInventoryWindow.ManualInventoryRefresh(),
- OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data),
+ OnEnd = _ => OnDragEnd?.Invoke(),
+ OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data),
OnRollOver = node =>
{
BeginHeaderHover();
@@ -254,49 +265,63 @@ public class InventoryCategoryNode : SimpleComponentNode
};
}
+ public void RefreshNodeVisuals()
+ {
+ foreach (var node in _itemGridNode.Nodes)
+ {
+ if (node is not InventoryDragDropNode itemNode || itemNode.ItemInfo == null) continue;
+
+ itemNode.Alpha = itemNode.ItemInfo.VisualAlpha;
+ itemNode.AddColor = itemNode.ItemInfo.HighlightOverlayColor;
+ itemNode.IsDraggable = !itemNode.ItemInfo.IsSlotBlocked;
+ }
+ }
+
private unsafe void OnDiscard(DragDropNode node, ItemInfo item)
{
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
AgentInventoryContext.Instance()->DiscardItem(item.Item.GetLinkedItem(), item.Item.Container, item.Item.Slot, addonId);
}
- private void OnPayloadAccepted(DragDropNode node, DragDropPayload payload, ItemInfo targetItemInfo)
+ private void OnPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload, ItemInfo targetItemInfo)
{
- InventoryItem item = targetItemInfo.Item;
- if (!payload.IsValidInventoryPayload)
+ try
{
- Services.Logger.Warning($"[OnPayload] Invalid payload type: {payload.Type}");
- return;
+ // KTK clears node.Payload before invoking this, so setting it manually again
+ var nodePayload = new DragDropPayload
+ {
+ Type = DragDropType.Item,
+ Int1 = targetItemInfo.VisualLocation.Container,
+ Int2 = targetItemInfo.VisualLocation.Slot,
+ ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
+ };
+
+ Services.Logger.DebugOnly($"[OnPayload] ACCEPTED payload: Type={acceptedPayload.Type} Int1={acceptedPayload.Int1} Int2={acceptedPayload.Int2} Ref={acceptedPayload.ReferenceIndex}");
+ Services.Logger.DebugOnly($"[OnPayload] NODE payload: Type={nodePayload.Type} Int1={nodePayload.Int1} Int2={nodePayload.Int2} Ref={nodePayload.ReferenceIndex}");
+
+ if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload)
+ {
+ Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}");
+ return;
+ }
+
+ if (acceptedPayload.IsSameBaseContainer(nodePayload))
+ {
+ Services.Logger.DebugOnly("[OnPayload] Source and target are in the same base container, skipping move.");
+ node.IconId = targetItemInfo.IconId;
+ node.Payload = nodePayload;
+ return;
+ }
+
+ var sourceCopy = acceptedPayload;
+ var targetCopy = nodePayload;
+
+ InventoryMoveHelper.HandleItemMovePayload(sourceCopy, targetCopy);
+ OnRefreshRequested?.Invoke();
}
-
- InventoryLocation sourceLocation = payload.InventoryLocation;
-
- if (!sourceLocation.IsValid)
+ catch (Exception ex)
{
- Services.Logger.Warning($"[OnPayload] Could not resolve source from payload");
- return;
+ Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
}
-
- InventoryLocation targetLocation = new InventoryLocation(
- item.Container,
- (ushort)item.Slot
- );
-
- if (sourceLocation.Container.IsSameContainerGroup(targetLocation.Container))
- {
- Services.Logger.Debug($"[OnPayload] Source and target are in the same container group; no move performed");
- node.Payload = payload;
- node.IconId = item.IconId;
- System.AddonInventoryWindow.ManualInventoryRefresh();
- return;
- };
-
- Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}");
-
- InventoryMoveHelper.MoveItem(
- sourceLocation.Container, sourceLocation.Slot,
- targetLocation.Container, targetLocation.Slot
- );
- System.AddonInventoryWindow.ManualInventoryRefresh();
}
}
\ No newline at end of file
diff --git a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
index a2435fb..285e68e 100644
--- a/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryDragDropNode.cs
@@ -1,17 +1,16 @@
using System.Numerics;
using AetherBags.Inventory;
+using AetherBags.Inventory.Items;
using Dalamud.Game.ClientState.Keys;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
-// TODO: Switch back to CS version when Dalamud Updated
-using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
namespace AetherBags.Nodes.Inventory;
-public class InventoryDragDropNode : DragDropFixedNode
+public class InventoryDragDropNode : DragDropNode
{
private readonly TextNode _quantityTextNode;
public unsafe InventoryDragDropNode()
diff --git a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs
index 27a9105..48024e3 100644
--- a/AetherBags/Nodes/Inventory/InventoryFooterNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryFooterNode.cs
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Numerics;
using AetherBags.Currency;
using AetherBags.Inventory;
+using AetherBags.Inventory.State;
using AetherBags.Nodes.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
@@ -24,7 +25,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode
FontType = FontType.MiedingerMed,
TextFlags = TextFlags.Glare,
TextColor = ColorHelper.GetColor(50),
- TextOutlineColor = ColorHelper.GetColor(32) // Could also be Color 65
+ TextOutlineColor = ColorHelper.GetColor(32)
};
_slotAmountTextNode.AttachNode(this);
@@ -46,8 +47,8 @@ public sealed class InventoryFooterNode : SimpleComponentNode
IReadOnlyList currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
_currencyListNode.SyncWithListDataByKey(
dataList: currencyInfoList,
- getKeyFromData: c => c.ItemId,
- getKeyFromNode: n => n.Currency.ItemId,
+ getKeyFromData: currencyInfo => currencyInfo.ItemId,
+ getKeyFromNode: node => node.Currency.ItemId,
updateNode: (node, data) =>
{
node.Currency = data;
diff --git a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs
index 02e578c..550dc72 100644
--- a/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs
+++ b/AetherBags/Nodes/Inventory/InventoryNotificationNode.cs
@@ -1,5 +1,6 @@
using System.Numerics;
using AetherBags.Inventory;
+using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines;
@@ -87,7 +88,7 @@ public sealed class InventoryNotificationNode : SimpleComponentNode
Timeline?.PlayAnimation(101);
}
- }
+ } = null!;
// Future Zeff, this always goes on a parent
private Timeline ParentLabels => new TimelineBuilder()
diff --git a/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs
new file mode 100644
index 0000000..1080390
--- /dev/null
+++ b/AetherBags/Nodes/Inventory/SaddleBagFooterNode.cs
@@ -0,0 +1,32 @@
+using System. Numerics;
+using FFXIVClientStructs.FFXIV.Component.GUI;
+using KamiToolKit.Classes;
+using KamiToolKit.Nodes;
+
+namespace AetherBags.Nodes.Inventory;
+
+public class SaddleBagFooterNode : SimpleComponentNode
+{
+ private readonly TextNode _slotCounterNode;
+
+ private const float Padding = 8f;
+
+ public SaddleBagFooterNode()
+ {
+ _slotCounterNode = new TextNode
+ {
+ Position = new Vector2(Padding, 4f),
+ Size = new Vector2(100, 20),
+ AlignmentType = AlignmentType.Left,
+ TextColor = new Vector4(1f, 1f, 1f, 1f),
+ FontSize = 14,
+ };
+ _slotCounterNode.AttachNode(this);
+ }
+
+ public string SlotAmountText
+ {
+ get => _slotCounterNode.String;
+ set => _slotCounterNode.String = $"Slots: {value}";
+ }
+}
\ No newline at end of file
diff --git a/AetherBags/Plugin.cs b/AetherBags/Plugin.cs
index 2be11b3..5199378 100644
--- a/AetherBags/Plugin.cs
+++ b/AetherBags/Plugin.cs
@@ -5,15 +5,18 @@ using AetherBags.Commands;
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
{
- private static string HelpDescription => "Opens your inventory.";
-
private readonly CommandHandler _commandHandler;
private readonly InventoryHooks _inventoryHooks;
private readonly InventoryLifecycles _inventoryLifecycles;
@@ -22,19 +25,35 @@ public unsafe class Plugin : IDalamudPlugin
{
pluginInterface.Create();
+ System.Config = Util.LoadConfigOrDefault();
+
BackupHelper.DoConfigBackup(pluginInterface);
KamiToolKitLibrary.Initialize(pluginInterface);
- System.Config = Util.LoadConfigOrDefault();
+ System.IPC = new IPCService();
System.AddonInventoryWindow = new AddonInventoryWindow
{
- InternalName = "AetherBags",
+ InternalName = "AetherBags_MainBags",
Title = "AetherBags",
Size = new Vector2(750, 750),
};
+ System.AddonSaddleBagWindow = new AddonSaddleBagWindow
+ {
+ InternalName = "AetherBags_SaddleBag",
+ Title = "AetherSaddlebag",
+ Size = new Vector2(750, 750),
+ };
+
+ System.AddonRetainerWindow = new AddonRetainerWindow
+ {
+ InternalName = "AetherBags_Retainer",
+ Title = "AetherRetainerbag",
+ Size = new Vector2(750, 750),
+ };
+
System.AddonConfigurationWindow = new AddonConfigurationWindow
{
InternalName = "AetherBags Config",
@@ -47,8 +66,6 @@ public unsafe class Plugin : IDalamudPlugin
_commandHandler = new CommandHandler();
- // Services.GameInventory.InventoryChanged += InventoryState.OnRawItemAdded;
-
Services.ClientState.Login += OnLogin;
Services.ClientState.Logout += OnLogout;
@@ -62,22 +79,20 @@ public unsafe class Plugin : IDalamudPlugin
public void Dispose()
{
- Util.SaveConfig(System.Config);
-
- // Services.GameInventory.InventoryChanged -= InventoryState.OnRawItemAdded;
-
- Services.ClientState.Login -= OnLogin;
- Services.ClientState.Logout -= OnLogout;
-
- _commandHandler.Dispose();
-
- System.AddonInventoryWindow.Dispose();
- System.AddonConfigurationWindow.Dispose();
-
- KamiToolKitLibrary.Dispose();
-
+ InventoryAddonContextMenu.Close();
_inventoryHooks.Dispose();
_inventoryLifecycles.Dispose();
+
+ System.IPC.Dispose();
+ HighlightState.ClearAll();
+
+ System.AddonInventoryWindow.Dispose();
+ System.AddonSaddleBagWindow.Dispose();
+ System.AddonRetainerWindow.Dispose();
+ System.AddonConfigurationWindow.Dispose();
+
+ Util.SaveConfig(System.Config);
+ KamiToolKitLibrary.Dispose();
}
private void OnLogin()
@@ -96,6 +111,8 @@ public unsafe class Plugin : IDalamudPlugin
Util.SaveConfig(System.Config);
InventoryState.TrackLootedItems = false;
System.AddonInventoryWindow.Close();
+ System.AddonSaddleBagWindow.Close();
+ System.AddonRetainerWindow.Close();
System.AddonConfigurationWindow.Close();
}
}
\ No newline at end of file
diff --git a/AetherBags/Services.cs b/AetherBags/Services.cs
index bf7a595..71fb89e 100644
--- a/AetherBags/Services.cs
+++ b/AetherBags/Services.cs
@@ -10,15 +10,17 @@ public class Services
[PluginService] public static IChatGui ChatGui { get; set; } = null!;
[PluginService] public static IClientState ClientState { get; private set; } = null!;
[PluginService] public static ICommandManager CommandManager { get; private set; } = null!;
+ [PluginService] public static ICondition Condition { get; private set; } = null!;
[PluginService] public static IDataManager DataManager { get; set; } = null!;
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService] public static IFramework Framework { get; private set; } = null!;
[PluginService] public static IGameGui GameGui { get; private set; } = null!;
[PluginService] public static IGameInventory GameInventory { get; set; } = null!;
[PluginService] public static IKeyState KeyState { get; private set; } = null!;
+ [PluginService] public static IPlayerState PlayerState { get; private set; } = null!;
[PluginService] public static IPluginLog Logger { get; private set; } = null!;
[PluginService] public static INotificationManager NotificationManager { get; private set; } = null!;
- // TODO: Remove cause temp
+ [PluginService] public static IObjectTable ObjectTable { get; private set; } = null!;
[PluginService] public static ISigScanner SigScanner { get; private set; } = null!;
[PluginService] public static IGameInteropProvider GameInteropProvider { get; private set; } = null!;
}
\ No newline at end of file
diff --git a/AetherBags/System.cs b/AetherBags/System.cs
index 05dee40..0821faa 100644
--- a/AetherBags/System.cs
+++ b/AetherBags/System.cs
@@ -1,11 +1,15 @@
using AetherBags.Addons;
using AetherBags.Configuration;
+using AetherBags.IPC;
namespace AetherBags;
public static class System
{
public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
+ public static AddonSaddleBagWindow AddonSaddleBagWindow { get; set; } = null!;
+ public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!;
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
+ public static IPCService IPC { get; set; } = null!;
public static SystemConfiguration Config { get; set; } = null!;
}
\ No newline at end of file
diff --git a/KamiToolKit b/KamiToolKit
index 2122482..1d838e8 160000
--- a/KamiToolKit
+++ b/KamiToolKit
@@ -1 +1 @@
-Subproject commit 2122482f0dd453a74227965b4f0a6868866e21c1
+Subproject commit 1d838e8bfa973a88389318c88e0a24e136976253