Merge remote-tracking branch 'origin/master' into dev/pie-lover

This commit is contained in:
Shawrkie Williams
2026-01-07 15:19:28 -05:00
83 changed files with 3839 additions and 1754 deletions
-10
View File
@@ -1,10 +0,0 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentInventory_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9e458ae8a124476099a99b081d71ce27826848_003F26_003F0a847424_003FAgentInventory_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentSatisfactionSupply_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fb2cd0663609440e590f52980cafc1ba3822648_003F28_003Ffa48b62e_003FAgentSatisfactionSupply_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAgentUpdateFlag_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7097e6bfd2288e8ff8dacc8d1e21863898453e58b9546b9752e0c0a5bed4dc_003FAgentUpdateFlag_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AAtkValue_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F1b966bf9f0d5b3eb39a7ee3ff6ab5c83f5bea8a841eafd7c8a1e55532d2d952_003FAtkValue_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003ACurrencyManager_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003Fe66db7cd515142b9bbfb1b4e18f82ace825448_003Ffc_003F78df30c7_003FCurrencyManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInventoryItem_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FDecompilerCache_003Fdecompiler_003F9e458ae8a124476099a99b081d71ce27826848_003F6f_003Ffa56b446_003FInventoryItem_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AInventoryManager_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F7d322ffbe41aca452171c1858ac4d72a967922191dfb8ada66667df5fd58b_003FInventoryManager_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AItemOrderModule_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F77e1e4eb417120eb1e45bbb81bf6a70e47ea3c097bdf493584be7f333fa_003FItemOrderModule_002Ecs/@EntryIndexedValue">ForceIncluded</s:String>
<s:String x:Key="/Default/CodeInspection/ExcludedFiles/FilesAndFoldersToSkip2/=7020124F_002D9FFC_002D4AC3_002D8F3D_002DAAB8E0240759_002Ff_003AItem_002Eg_002Ecs_002Fl_003AC_0021_003FUsers_003FJeffro_003FAppData_003FRoaming_003FJetBrains_003FRider2025_002E3_003Fresharper_002Dhost_003FSourcesCache_003F8ec7cc8a18dbb6a6f3c21f8adcb4e2661dc7979_003FItem_002Eg_002Ecs/@EntryIndexedValue">ForceIncluded</s:String></wpf:ResourceDictionary>
@@ -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);
}
}
@@ -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;
@@ -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()
@@ -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);
+30 -266
View File
@@ -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<InventoryCategoryNode> _hoverSubscribed = new();
private readonly MainBagState _inventoryState = new();
private InventoryNotificationNode _notificationNode = null!;
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
private TextInputWithHintNode _searchInputNode = null!;
private CircleButtonNode _settingsButtonNode = null!;
private InventoryFooterNode _footerNode = null!;
// Window constraints
private const float MinWindowWidth = 300;
private const float MaxWindowWidth = 800;
private const float MinWindowHeight = 200;
private const float MaxWindowHeight = 1000;
protected override InventoryStateBase InventoryState => _inventoryState;
// Layout settings
private const float CategorySpacing = 12;
private const float ItemSize = 40;
private const float ItemPadding = 4;
private const float FooterHeight = 28f;
private const float FooterTopSpacing = 4f;
private bool _refreshQueued;
private bool _refreshAutosizeQueued;
protected override unsafe void OnSetup(AtkUnitBase* addon)
protected override void OnSetup(AtkUnitBase* addon)
{
_categoriesNode = new WrappingGridNode<InventoryCategoryNode>
InitializeBackgroundDropTarget();
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{
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<LootedItemInfo> lootedItemInfos)
{
if (!Services.ClientState.IsLoggedIn) return;
_recentlyLootedCategoryNode?.CategorizedInventory.Items.AddRange(
lootedItemInfos.Select(x => new ItemInfo
{
ItemCount = x.Quantity,
Key = uint.MaxValue - 1,
Item = x.Item,
})
.ToList());
RefreshCategoriesCore(true);
}*/
public void ManualCurrencyRefresh()
{
if (!Services.ClientState.IsLoggedIn) return;
_footerNode.RefreshCurrencies();
FooterNode.RefreshCurrencies();
}
private void OnInventoryUpdate(AddonEvent type, AddonArgs args)
{
InventoryState.RefreshFromGame();
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<CategorizedInventory> categories = InventoryState.GetInventoryItemCategories(filter);
float maxContentWidth = MaxWindowWidth - (ContentStartPosition.X * 2);
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
_categoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
dataList: categories,
getKeyFromData: c => c.Key,
getKeyFromNode: n => n.CategorizedInventory.Key,
updateNode: (node, data) =>
{
node.CategorizedInventory = data;
node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
},
createNodeMethod: _ => new InventoryCategoryNode
{
Size = ContentSize with { Y = 120 },
});
bool pinsChanged = _pinCoordinator.ApplyPinnedStates(_categoriesNode);
if (pinsChanged)
_hoverCoordinator.ResetAll(_categoriesNode);
WireHoverHandlers();
if (autosize) AutoSizeWindow();
else
{
LayoutContent();
_categoriesNode.RecalculateLayout();
}
}
private void WireHoverHandlers()
{
var nodes = _categoriesNode.Nodes;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode node)
continue;
if (!_hoverSubscribed.Add(node))
continue;
node.HeaderHoverChanged += (src, hovering) =>
{
_hoverCoordinator.OnCategoryHoverChanged(_categoriesNode, src, hovering);
};
}
}
private int CalculateOptimalItemsPerLine(float availableWidth)
{
return Math.Clamp((int)MathF.Floor((availableWidth + ItemPadding) / (ItemSize + ItemPadding)), 1, 15);
}
private void LayoutContent()
{
Vector2 contentPos = ContentStartPosition;
Vector2 contentSize = ContentSize;
float footerH = FooterHeight;
_footerNode.Position = new Vector2(contentPos.X, contentPos.Y + contentSize.Y - footerH);
_footerNode.Size = new Vector2(contentSize.X, footerH);
float gridH = contentSize.Y - footerH - FooterTopSpacing;
if (gridH < 0) gridH = 0;
_categoriesNode.Position = contentPos;
_categoriesNode.Size = new Vector2(contentSize.X, gridH);
}
private void AutoSizeWindow()
{
var nodes = _categoriesNode.Nodes;
float maxChildWidth = 0f;
int childCount = 0;
for (int i = 0; i < nodes.Count; i++)
{
if (nodes[i] is not InventoryCategoryNode cat)
continue;
childCount++;
float w = cat.Width;
if (w > maxChildWidth) maxChildWidth = w;
}
if (childCount == 0)
{
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
return;
}
float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
float gridBudget = Math.Max(0f, MaxWindowHeight - FooterHeight - FooterTopSpacing);
_categoriesNode.Position = ContentStartPosition;
_categoriesNode.Size = new Vector2(contentWidth, gridBudget);
_categoriesNode.RecalculateLayout();
float requiredGridHeight = _categoriesNode.GetRequiredHeight();
float requiredContentHeight = requiredGridHeight + FooterTopSpacing + FooterHeight;
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
}
private void ResizeWindow(float width, float height, bool recalcLayout)
{
SetWindowSize(width, height);
LayoutContent();
if (recalcLayout)
_categoriesNode.RecalculateLayout();
}
private void ResizeWindow(float width, float height)
=> ResizeWindow(width, height, recalcLayout: true);
public void SetNotification(InventoryNotificationInfo info)
{
Services.Framework.RunOnTick(() =>
{
if (IsOpen) _notificationNode.NotificationInfo = info;
}, delayTicks: 1);
}, 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);
}
}
+188
View File
@@ -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<InventoryCategoryNode>
{
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);
}
}
+116
View File
@@ -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<InventoryCategoryNode>
{
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);
}
}
+1
View File
@@ -1,5 +1,6 @@
using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using KamiToolKit.Premade;
namespace AetherBags.Addons;
+11
View File
@@ -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);
}
+437
View File
@@ -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<InventoryCategoryNode> HoverSubscribed = new();
protected DragDropNode BackgroundDropTarget = null!;
protected WrappingGridNode<InventoryCategoryNode> 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<uint> _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<CategorizedInventory, InventoryCategoryNode, uint>(
dataList: categories,
getKeyFromData: categorizedInventory => categorizedInventory.Key,
getKeyFromNode: node => node.CategorizedInventory.Key,
updateNode: (node, data) =>
{
node.CategorizedInventory = data;
node.ItemsPerLine = Math.Min(data.Items.Count, maxItemsPerLine);
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);
}
}
@@ -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();
}
}
}
+15 -5
View File
@@ -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();
@@ -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<UserCategoryDefinition> UserCategories { get; set; } = new();
}
@@ -76,3 +80,9 @@ public enum ToggleFilterState
Allow = 1,
Disallow = 2,
}
public enum AllaganToolsFilterMode
{
Categorize = 0,
Highlight = 1,
}
@@ -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;
}
@@ -19,3 +24,9 @@ public enum InventoryStackMode : byte
NaturalStacks = 0,
AggregateByItemId = 1,
}
public enum SearchMode : byte
{
Filter = 0,
Highlight = 1,
}
@@ -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);
}
}
}
@@ -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);
}
}
}
}
@@ -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
@@ -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);
}
}
@@ -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;
+8 -5
View File
@@ -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));
}
}
+1 -1
View File
@@ -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) {
+26 -19
View File
@@ -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[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);
}
*/
}
+11 -10
View File
@@ -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);
}
+195
View File
@@ -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<bool>? _isInitialized;
private ICallGateSubscriber<bool, bool>? _initialized;
private ICallGateSubscriber<string, Dictionary<uint, uint>>? _getFilterItems;
private ICallGateSubscriber<Dictionary<string, string>>? _getSearchFilters;
private ICallGateSubscriber<string, bool>? _enableUiFilter;
private ICallGateSubscriber<string, bool>? _toggleUiFilter;
public bool IsReady { get; private set; }
/// <summary>
/// Cached filter items. Key = filterKey, Value = (ItemId -> Quantity).
/// </summary>
public Dictionary<string, Dictionary<uint, uint>> CachedFilterItems { get; } = new();
/// <summary>
/// Cached search filters. Key -> Name.
/// </summary>
public Dictionary<string, string> CachedSearchFilters { get; } = new();
/// <summary>
/// Quick lookup: ItemId -> List of filter keys that contain this item.
/// </summary>
public Dictionary<uint, List<string>> ItemToFilters { get; } = new();
public event Action? OnInitialized;
public event Action? OnFiltersRefreshed;
public AllaganToolsIPC()
{
try
{
_isInitialized = Services.PluginInterface.GetIpcSubscriber<bool>("AllaganTools.IsInitialized");
_initialized = Services.PluginInterface.GetIpcSubscriber<bool, bool>("AllaganTools.Initialized");
_getFilterItems = Services.PluginInterface.GetIpcSubscriber<string, Dictionary<uint, uint>>("AllaganTools.GetFilterItems");
_getSearchFilters = Services.PluginInterface.GetIpcSubscriber<Dictionary<string, string>>("AllaganTools.GetSearchFilters");
_enableUiFilter = Services.PluginInterface.GetIpcSubscriber<string, bool>("AllaganTools.EnableUiFilter");
_toggleUiFilter = Services.PluginInterface.GetIpcSubscriber<string, bool>("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();
}
}
/// <summary>
/// Refreshes all cached filter data from Allagan Tools.
/// Call this when you need updated filter information.
/// </summary>
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<string>(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}");
}
}
/// <summary>
/// Checks if an item is in any Allagan Tools filter.
/// </summary>
public bool IsItemInAnyFilter(uint itemId)
=> ItemToFilters.ContainsKey(itemId);
/// <summary>
/// Gets all filter keys that contain this item.
/// </summary>
public IReadOnlyList<string>? GetFiltersForItem(uint itemId)
=> ItemToFilters.TryGetValue(itemId, out var list) ? list : null;
/// <summary>
/// Gets items from a specific filter. Returns ItemId -> Quantity.
/// </summary>
public Dictionary<uint, uint>? 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;
}
}
/// <summary>
/// Gets all available search filters. Returns Key -> Name.
/// </summary>
public Dictionary<string, string>? 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);
}
}
+79
View File
@@ -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<bool>? _isInitialized;
private ICallGateSubscriber<bool, bool>? _initialized;
private ICallGateSubscriber<List<uint>>? _getBisItems;
private ICallGateSubscriber<List<uint>, 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<bool>("BisBuddy.IsInitialized");
_initialized = Services.PluginInterface.GetIpcSubscriber<bool, bool>("BisBuddy.Initialized");
_getBisItems = Services.PluginInterface.GetIpcSubscriber<List<uint>>("BisBuddy.GetBisItems");
_bisItemsChanged = Services.PluginInterface.GetIpcSubscriber<List<uint>, 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<uint>? 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);
}
}
+19
View File
@@ -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();
}
}
+80
View File
@@ -0,0 +1,80 @@
using System;
using Dalamud.Plugin.Ipc;
namespace AetherBags.IPC;
public class WotsItIPC : IDisposable
{
private ICallGateSubscriber<string, string, string, uint, string>? _registerWithSearch;
private ICallGateSubscriber<string, bool>? _invoke;
private ICallGateSubscriber<string, bool>? _unregisterAll;
private string? _searchGuid;
public WotsItIPC()
{
try
{
_registerWithSearch = Services.PluginInterface.GetIpcSubscriber<string, string, string, uint, string>("FA.RegisterWithSearch");
_unregisterAll = Services.PluginInterface.GetIpcSubscriber<string, bool>("FA.UnregisterAll");
_invoke = Services.PluginInterface.GetIpcSubscriber<string, bool>("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();
}
}
@@ -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<AtkDragDropInterfaceFixed*, float*, float*, void>)VirtualTable[1];
fnPtr(ThisPtr, screenX, screenY);
}
// [VirtualFunction(3)]
public AtkComponentNode* GetComponentNode()
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkComponentNode*>)VirtualTable[3];
return fnPtr(ThisPtr);
}
// [VirtualFunction(5)]
public void SetComponentNode(AtkComponentNode* node)
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkComponentNode*, void>)VirtualTable[5];
fnPtr(ThisPtr, node);
}
// [VirtualFunction(6)]
public AtkResNode* GetActiveNode()
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkResNode*>)VirtualTable[6];
return fnPtr(ThisPtr);
}
// [VirtualFunction(8)]
public AtkComponentBase* GetComponent()
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkComponentBase*>)VirtualTable[8];
return fnPtr(ThisPtr);
}
// [VirtualFunction(9)]
public bool HandleMouseUpEvent(AtkEventData.AtkMouseData* mouseData)
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkEventData.AtkMouseData*, byte>)VirtualTable[9];
return fnPtr(ThisPtr, mouseData) != 0;
}
// [VirtualFunction(12)]
public AtkDragDropPayloadContainer* GetPayloadContainer()
{
var fnPtr = (delegate* unmanaged<AtkDragDropInterfaceFixed*, AtkDragDropPayloadContainer*>)VirtualTable[12];
return fnPtr(ThisPtr);
}
}
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List<ItemInfo> Items);
@@ -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<ItemInfo> Items = null!;
public List<ItemInfo> FilteredItems = null!;
public bool Used;
}
public sealed class ItemCountDescComparer : IComparer<ItemInfo>
{
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;
}
}
@@ -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;
/// <summary>
/// Resets all buckets for a new refresh cycle.
/// </summary>
@@ -147,6 +158,74 @@ public static class CategoryBucketManager
}
}
public static void BucketByAllaganFilters(
Dictionary<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey,
HashSet<ulong> 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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> 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);
});
}
@@ -276,31 +359,3 @@ public static class CategoryBucketManager
};
}
}
public sealed class CategoryBucket
{
public uint Key;
public CategoryInfo Category = null!;
public List<ItemInfo> Items = null!;
public List<ItemInfo> FilteredItems = null!;
public bool Used;
}
public sealed class ItemCountDescComparer : IComparer<ItemInfo>
{
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;
}
}
@@ -1,7 +1,7 @@
using System.Numerics;
using KamiToolKit.Classes;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public class CategoryInfo
{
@@ -1,8 +1,9 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
public static class InventoryFilter
{
@@ -1,8 +1,9 @@
using AetherBags.Configuration;
using System;
using System.Text.RegularExpressions;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Categories;
internal static class UserCategoryMatcher
{
@@ -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<HighlightSource, HashSet<uint>> Filters = new();
private static readonly Dictionary<HighlightSource, (HashSet<uint> 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<uint> ids)
=> Filters[source] = new HashSet<uint>(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<uint> ids, Vector3 color)
=> Labels[source] = (new HashSet<uint>(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);
}
@@ -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<InventoryMappedLocation, InventoryMappedLocation> VisualLocationMap = new();
private static readonly Dictionary<int, Dictionary<InventoryMappedLocation, InventoryMappedLocation>> 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);
}
}
@@ -2,7 +2,7 @@ using System.Collections.Generic;
using Lumina.Excel.Sheets;
using Lumina.Text.ReadOnly;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Context;
public class InventoryNotificationState
{
@@ -1,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<InventoryMappedLocation, InventoryMappedLocation> 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);
}
+1 -1
View File
@@ -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;
@@ -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<IInventoryWindow> GetAllWindows()
{
yield return System.AddonInventoryWindow;
yield return System.AddonSaddleBagWindow;
yield return System.AddonRetainerWindow;
}
}
@@ -1,4 +1,4 @@
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public readonly struct InventoryStats
{
@@ -1,11 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using System;
using System.Numerics;
using System.Text.RegularExpressions;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public sealed class ItemInfo : IEquatable<ItemInfo>
{
@@ -57,15 +58,13 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
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<ItemInfo>
{
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)
@@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory;
namespace AetherBags.Inventory.Items;
public record LootedItemInfo(int Index, InventoryItem Item, int Quantity);
@@ -0,0 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.Scanning;
public struct AggregatedItem
{
public InventoryItem First;
public int Total;
}
@@ -1,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<ulong, AggregatedItem> aggByKey)
Dictionary<ulong, AggregatedItem> aggByKey,
InventorySourceType source)
{
aggByKey.Clear();
var inventories = InventorySourceDefinitions.GetInventories(source);
int scannedSlots = 0;
int nonEmptySlots = 0;
int collisions = 0;
for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++)
for (int inventoryIndex = 0; inventoryIndex < inventories.Length; inventoryIndex++)
{
var inventoryType = BagInventories[inventoryIndex];
var inventoryType = inventories[inventoryIndex];
var container = inventoryManager->GetInventoryContainer(inventoryType);
if (container == null)
{
@@ -164,16 +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);
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);
}
}
public struct AggregatedItem
{
public InventoryItem First;
public int Total;
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;
}
}
@@ -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,
};
}
@@ -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<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
private static IReadOnlyList<InventoryType> StandardInventories => InventoryScanner.StandardInventories;
private static readonly Dictionary<ulong, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
@@ -28,68 +31,6 @@ public static unsafe class InventoryState
public static bool Contains(this IReadOnlyCollection<InventoryType> 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<UserCategoryDefinition> 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<CategorizedInventory> 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<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
=> CurrencyState.GetCurrencyInfoList(currencyIds);
@@ -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<ulong, AggregatedItem> AggByKey = new(capacity: 512);
protected readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = new(capacity: 512);
protected readonly Dictionary<uint, CategoryBucket> BucketsByKey = new(capacity: 256);
protected readonly List<uint> SortedCategoryKeys = new(capacity: 256);
protected readonly List<CategorizedInventory> AllCategories = new(capacity: 256);
protected readonly List<CategorizedInventory> FilteredCategories = new(capacity: 256);
protected readonly List<UserCategoryDefinition> UserCategoriesSortedScratch = new(capacity: 64);
protected readonly List<ulong> RemoveKeysScratch = new(capacity: 256);
protected readonly HashSet<ulong> ClaimedKeys = new(capacity: 512);
public abstract InventorySourceType SourceType { get; }
public abstract InventoryType[] Inventories { get; }
public virtual unsafe void RefreshFromGame()
{
InventoryManager* inventoryManager = InventoryManager.Instance();
if (inventoryManager == null)
{
ClearAll();
return;
}
var config = 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<CategorizedInventory> GetCategories(string filter = "", bool invert = false)
=> InventoryFilter.FilterCategories(AllCategories, BucketsByKey, FilteredCategories, filter, invert);
public string GetEmptySlotsString() => InventoryScanner.GetEmptySlotsString(SourceType);
protected virtual void ClearAll()
{
AggByKey.Clear();
ItemInfoByKey.Clear();
foreach (var kvp in BucketsByKey)
{
kvp.Value.Items.Clear();
kvp.Value.FilteredItems.Clear();
kvp.Value.Used = false;
}
SortedCategoryKeys.Clear();
AllCategories.Clear();
FilteredCategories.Clear();
RemoveKeysScratch.Clear();
ClaimedKeys.Clear();
}
}
@@ -0,0 +1,17 @@
using AetherBags.Inventory.Context;
using AetherBags.Inventory.Scanning;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.State;
public class MainBagState : InventoryStateBase
{
public override InventorySourceType SourceType => InventorySourceType.MainBags;
public override InventoryType[] Inventories => InventorySourceDefinitions.MainBags;
protected override void OnPostScan()
{
InventoryContextState.RefreshMaps();
InventoryContextState.RefreshBlockedSlots();
}
}
@@ -0,0 +1,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;
}
@@ -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;
}
}
}
+3 -2
View File
@@ -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<Vector4>? OnColorConfirmed { get; set; }
public Action<Vector4>? OnColorCanceled { get; set; }
public Action<Vector4>? OnColorChange { get; set; }
public Action<Vector4>? OnColorPreviewed { get; set; }
}
@@ -7,67 +7,52 @@ namespace AetherBags.Nodes.Configuration.Category;
public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
{
private readonly ScrollingAreaNode<VerticalListNode> _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode;
public Action? OnCategoryChanged { get; set; }
public CategoryConfigurationNode()
{
_categoryList = new ScrollingAreaNode<VerticalListNode>
{
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);
_activeNode.AttachNode(this);
}
else
{
_activeNode.IsVisible = true;
_activeNode.Size = Size;
_activeNode.SetCategory(option.CategoryDefinition);
}
UpdateScrollHeight();
}
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<CategoryDefinitionConfigurationNode>())
if (_activeNode is not null)
{
node.Width = Width;
}
UpdateScrollHeight();
_activeNode.Size = Size;
}
}
}
@@ -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<Item>? ItemSheet => Services.DataManager.GetExcelSheet<Item>();
private static ExcelSheet<ItemUICategory>? UICategorySheet => Services.DataManager.GetExcelSheet<ItemUICategory>();
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<TreeListNode> _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<Item>? _sItemSheet;
private static ExcelSheet<ItemUICategory>? _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<Item>();
_sUICategorySheet ??= Services.DataManager.GetExcelSheet<ItemUICategory>();
FitContents = true;
ItemSpacing = 4.0f;
var catchAllWarningNode = new TextNode
_scrollingArea = new ScrollingAreaNode<TreeListNode>
{
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<UserCategoryDefinition> _getCategoryDefinition;
public Action? OnValueChanged { get; init; }
protected UserCategoryDefinition CategoryDefinition => _getCategoryDefinition();
protected ConfigurationSection(Func<UserCategoryDefinition> 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<UserCategoryDefinition> 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<UserCategoryDefinition> 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<int> filter,
int minBound,
int maxBound,
Action<bool, int, int> onUpdate)
_vendorPriceFilter = new RangeFilterRowUint
{
var enabledCheckbox = new CheckboxNode
Label = "Vendor Price",
MinBound = 0,
MaxBound = 9_999_999,
OnFilterChanged = (enabled, min, max) =>
{
Size = new Vector2(200, 20),
String = $"{label} Filter",
IsChecked = filter.Enabled,
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<uint> 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<int> 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<UserCategoryDefinition, StateFilter> GetFilter)> _filters = [];
private bool _initialized;
public StateFiltersSection(Func<UserCategoryDefinition> 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<UserCategoryDefinition, StateFilter> 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<UserCategoryDefinition> 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();
}
}
@@ -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<AllaganToolsFilterMode>(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);
}
@@ -4,7 +4,7 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category;
public class CategoryScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
public sealed class CategoryScrollingAreaNode : ScrollingListNode
{
private AddonCategoryConfigurationWindow? _categoryConfigurationAddon;
private readonly TextButtonNode _categoryConfigurationButtonNode;
@@ -13,13 +13,15 @@ public class CategoryScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{
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() {
@@ -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<bool, int, int>? 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<int> 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<bool, uint, uint>? 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<uint> 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;
}
}
@@ -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
{
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<int> _list;
private readonly List<CheckboxNode> _checkboxes = new();
private readonly Action? _onChanged;
private static readonly string[] RarityNames =
[
"Common (White)",
"Uncommon (Green)",
"Rare (Blue)",
"Relic (Purple)",
"Aetherial (Pink)"
];
public RarityEditorNode(List<int> list, Action? onChanged = null)
public Action? OnChanged { get; set; }
private List<int> _list = [];
private readonly List<CheckboxNode> _checkboxes = [];
public RarityEditorNode()
{
_list = list;
_onChanged = onChanged;
FitContents = true;
ItemSpacing = 2.0f;
@@ -32,15 +40,21 @@ 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 =>
OnClick = isChecked => ToggleRarity(rarity, isChecked),
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
}
private void ToggleRarity(int rarity, bool isChecked)
{
if (isChecked && !_list.Contains(rarity))
{
@@ -51,12 +65,8 @@ public sealed class RarityEditorNode : VerticalListNode
{
_list.Remove(rarity);
}
_onChanged?.Invoke();
},
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
OnChanged?.Invoke();
}
public void SetList(List<int> 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);
}
@@ -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)
{
_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<int>
{
private static readonly string[] StateLabels = ["Ignored", "Required", "Excluded"];
protected override string GetStateText(int state)
=> state >= 0 && state < StateLabels.Length ?StateLabels[state] : "Unknown";
}
}
@@ -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<string> _list;
private readonly TextInputNode _addInput;
private const float LabelWidth = 300f;
private const float RowHeight = 28f;
private List<string> _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<string> 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<string> 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)
_itemsContainer.Clear();
foreach (var value in _list)
{
Size = new Vector2(280, 22),
OnRemove = () =>
{
_list.Remove(value);
RefreshItems();
_onChanged?.Invoke();
},
_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);
});
}
}
@@ -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<uint> _list;
private readonly NumericInputNode _addInput;
private const float LabelWidth = 300f;
private const float RowHeight = 28f;
private List<uint> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged;
private readonly Func<uint, string>? _labelResolver;
private readonly HorizontalListNode _addRow;
private readonly NumericInputNode _addInput;
public UintListEditorNode(string label, List<uint> list, Action? onChanged = null, Func<uint, string>? labelResolver = null)
public Func<uint, string>? 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<uint> 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)
_itemsContainer.Clear();
foreach (var value in _list)
{
Size = new Vector2(280, 22),
OnRemove = () =>
{
_list.Remove(value);
RefreshItems();
_onChanged?.Invoke();
},
_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
{
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);
});
}
}
@@ -1,7 +0,0 @@
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration;
internal class ConfigurationRoot : TabbedVerticalListNode
{
}
@@ -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,
@@ -2,11 +2,11 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Currency;
public sealed class CurrencyScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
public sealed class CurrencyScrollingAreaNode : ScrollingListNode
{
public CurrencyScrollingAreaNode()
{
ContentNode.AddNode(new CurrencyGeneralConfigurationNode
AddNode(new CurrencyGeneralConfigurationNode
{
Size = Size
});
@@ -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<SearchMode>(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<InventoryStackMode>(selected, out var parsed))
{
config.StackMode = parsed;
System.AddonInventoryWindow.ManualInventoryRefresh();
InventoryOrchestrator.RefreshAll(updateMaps: true);
}
}
};
@@ -5,30 +5,30 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.General;
public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
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();
}
@@ -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);
}
@@ -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);
@@ -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);
-242
View File
@@ -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<AtkComponentDragDrop, AtkUldComponentDataDragDrop> {
// 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<DragDropNode>? OnBegin { get; set; }
public Action<DragDropNode>? OnEnd { get; set; }
public Action<DragDropNode, DragDropPayload>? OnPayloadAccepted { get; set; }
public Action<DragDropNode>? OnDiscard { get; set; }
public Action<DragDropNode>? OnClicked { get; set; }
public Action<DragDropNode>? OnRollOver { get; set; }
public Action<DragDropNode>? 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());
}
}
@@ -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;
@@ -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<ReadOnlySeString>? 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;
}
}
@@ -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<ReadOnlySeString>? 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;
}
}
@@ -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<DragDropFixedNode> _itemGridNode;
private readonly HybridDirectionalFlexNode<DragDropNode> _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<InventoryCategoryNode, bool>? 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<DragDropFixedNode>
_itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
{
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;
}
InventoryLocation sourceLocation = payload.InventoryLocation;
if (!sourceLocation.IsValid)
// KTK clears node.Payload before invoking this, so setting it manually again
var nodePayload = new DragDropPayload
{
Services.Logger.Warning($"[OnPayload] Could not resolve source from payload");
return;
}
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;
Type = DragDropType.Item,
Int1 = targetItemInfo.VisualLocation.Container,
Int2 = targetItemInfo.VisualLocation.Slot,
ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
};
Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}");
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}");
InventoryMoveHelper.MoveItem(
sourceLocation.Container, sourceLocation.Slot,
targetLocation.Container, targetLocation.Slot
);
System.AddonInventoryWindow.ManualInventoryRefresh();
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();
}
catch (Exception ex)
{
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
}
}
}
@@ -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()
@@ -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<CurrencyInfo> currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
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;
@@ -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()
@@ -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}";
}
}
+37 -20
View File
@@ -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<Services>();
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();
}
}
+3 -1
View File
@@ -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!;
}
+4
View File
@@ -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!;
}