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 System.Linq;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using Dalamud.Game.Addon.Lifecycle; using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Game.NativeWrapper; using Dalamud.Game.NativeWrapper;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
using ValueType = FFXIVClientStructs.FFXIV.Component.GUI.ValueType;
namespace AetherBags.AddonLifecycles; namespace AetherBags.AddonLifecycles;
@@ -16,10 +20,90 @@ public class InventoryLifecycles : IDisposable
public InventoryLifecycles() 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"); 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[0] = OpenType
values[1] = OpenTitleId values[1] = OpenTitleId
@@ -31,14 +115,17 @@ public class InventoryLifecycles : IDisposable
values[7] = can use Saddlebags (Agent InventoryBuddy IsActivatable) 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) if (args is not AddonRefreshArgs refreshArgs)
return; return;
if (IsInUnsafeState())
return;
GeneralSettings config = System.Config.General; 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(); AtkValuePtr[] atkValues = refreshArgs.AtkValueEnumerable.ToArray();
@@ -48,6 +135,9 @@ public class InventoryLifecycles : IDisposable
AtkValue* value5 = (AtkValue*)atkValues[5].Address; AtkValue* value5 = (AtkValue*)atkValues[5].Address;
AtkValue* value6 = (AtkValue*)atkValues[6].Address; AtkValue* value6 = (AtkValue*)atkValues[6].Address;
if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString)
return;
int openTitleId = value1->Int; int openTitleId = value1->Int;
ReadOnlySeString title = value5->String.AsReadOnlySeString(); ReadOnlySeString title = value5->String.AsReadOnlySeString();
ReadOnlySeString upperTitle = value6->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() 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, ["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.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Nodes.Configuration.Category; using AetherBags.Nodes.Configuration.Category;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit; using KamiToolKit;
@@ -118,7 +119,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
listNode.AddOption(newWrapper); listNode.AddOption(newWrapper);
RefreshSelectionList(); RefreshSelectionList();
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void OnRemoveCategory(CategoryWrapper categoryWrapper) private void OnRemoveCategory(CategoryWrapper categoryWrapper)
@@ -134,7 +135,7 @@ public class AddonCategoryConfigurationWindow : NativeAddon
{ {
OnOptionChanged(null); OnOptionChanged(null);
} }
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
private void RefreshSelectionList() private void RefreshSelectionList()
@@ -37,8 +37,6 @@ public class AddonConfigurationWindow : NativeAddon
{ {
Position = ContentStartPosition with { Y = tabContentY }, Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight }, Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = true, IsVisible = true,
}; };
_generalScrollingAreaNode.AttachNode(this); _generalScrollingAreaNode.AttachNode(this);
@@ -47,8 +45,6 @@ public class AddonConfigurationWindow : NativeAddon
{ {
Position = ContentStartPosition with { Y = tabContentY }, Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight }, Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = false, IsVisible = false,
}; };
_categoryScrollingAreaNode.AttachNode(this); _categoryScrollingAreaNode.AttachNode(this);
@@ -57,8 +53,6 @@ public class AddonConfigurationWindow : NativeAddon
{ {
Position = ContentStartPosition with { Y = tabContentY }, Position = ContentStartPosition with { Y = tabContentY },
Size = ContentSize with { Y = tabContentHeight }, Size = ContentSize with { Y = tabContentHeight },
ContentHeight = 400,
ScrollSpeed = 25,
IsVisible = false, IsVisible = false,
}; };
_currencyScrollingAreaNode.AttachNode(this); _currencyScrollingAreaNode.AttachNode(this);
+30 -266
View File
@@ -1,53 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Inventory; using AetherBags.Inventory.Context;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Input; using AetherBags.Nodes.Input;
using AetherBags.Nodes.Inventory; using AetherBags.Nodes.Inventory;
using AetherBags.Nodes.Layout; using AetherBags.Nodes.Layout;
using Dalamud.Game.Addon.Lifecycle;
using Dalamud.Game.Addon.Lifecycle.AddonArgTypes;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Addons; namespace AetherBags.Addons;
public class AddonInventoryWindow : NativeAddon public unsafe class AddonInventoryWindow : InventoryAddonBase
{ {
private readonly InventoryCategoryHoverCoordinator _hoverCoordinator = new(); private readonly MainBagState _inventoryState = new();
private readonly InventoryCategoryPinCoordinator _pinCoordinator = new();
private readonly HashSet<InventoryCategoryNode> _hoverSubscribed = new();
private InventoryNotificationNode _notificationNode = null!; private InventoryNotificationNode _notificationNode = null!;
private WrappingGridNode<InventoryCategoryNode> _categoriesNode = null!;
private TextInputWithHintNode _searchInputNode = null!;
private CircleButtonNode _settingsButtonNode = null!;
private InventoryFooterNode _footerNode = null!;
// Window constraints protected override InventoryStateBase InventoryState => _inventoryState;
private const float MinWindowWidth = 300;
private const float MaxWindowWidth = 800;
private const float MinWindowHeight = 200;
private const float MaxWindowHeight = 1000;
// Layout settings protected override void OnSetup(AtkUnitBase* addon)
private const float CategorySpacing = 12;
private const float ItemSize = 40;
private const float ItemPadding = 4;
private const float FooterHeight = 28f;
private const float FooterTopSpacing = 4f;
private bool _refreshQueued;
private bool _refreshAutosizeQueued;
protected override unsafe void OnSetup(AtkUnitBase* addon)
{ {
_categoriesNode = new WrappingGridNode<InventoryCategoryNode> InitializeBackgroundDropTarget();
CategoriesNode = new WrappingGridNode<InventoryCategoryNode>
{ {
Position = ContentStartPosition, Position = ContentStartPosition,
Size = ContentSize, Size = ContentSize,
@@ -56,276 +31,69 @@ public class AddonInventoryWindow : NativeAddon
TopPadding = 4.0f, TopPadding = 4.0f,
BottomPadding = 4.0f, BottomPadding = 4.0f,
}; };
_categoriesNode.AttachNode(this); CategoriesNode.AttachNode(this);
var size = new Vector2(addon->Size.X / 2.0f, 28.0f); var header = CalculateHeaderLayout(addon);
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;
_notificationNode = new InventoryNotificationNode _notificationNode = new InventoryNotificationNode
{ {
Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f), Position = new Vector2(WindowNode!.X - 4f, WindowNode!.Y - 32f),
Size = new Vector2(headerW, 28f), Size = new Vector2(header.HeaderWidth, 28f),
}; };
_notificationNode.AttachNode(this); _notificationNode.AttachNode(this);
_searchInputNode = new TextInputWithHintNode SearchInputNode = new TextInputWithButtonNode
{ {
Position = new Vector2(x, y), Position = header.SearchPosition,
Size = size, Size = header.SearchSize,
OnInputReceived = _ => RefreshCategoriesCore(autosize: false), 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), Size = new Vector2(28f),
Icon = ButtonIcon.GearCog, Icon = ButtonIcon.GearCog,
OnClick = System.AddonConfigurationWindow.Toggle OnClick = System.AddonConfigurationWindow.Toggle
}; };
_settingsButtonNode.AttachNode(this); SettingsButtonNode.AttachNode(this);
_footerNode = new InventoryFooterNode FooterNode = new InventoryFooterNode
{ {
Size = ContentSize with { Y = FooterHeight }, Size = ContentSize with { Y = FooterHeight },
SlotAmountText = InventoryState.GetEmptyItemSlotsString(), SlotAmountText = _inventoryState.GetEmptySlotsString(),
}; };
_footerNode.AttachNode(this); FooterNode.AttachNode(this);
LayoutContent(); LayoutContent();
Services.AddonLifecycle.RegisterListener(AddonEvent.PostRequestedUpdate, "Inventory", OnInventoryUpdate);
addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); addon->SubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
InventoryState.RefreshFromGame(); _isSetupComplete = true;
_inventoryState.RefreshFromGame();
RefreshCategoriesCore(autosize: true); RefreshCategoriesCore(autosize: true);
base.OnSetup(addon); 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() public void ManualCurrencyRefresh()
{ {
if (!Services.ClientState.IsLoggedIn) return; 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) public void SetNotification(InventoryNotificationInfo info)
{ {
Services.Framework.RunOnTick(() => Services.Framework.RunOnTick(() =>
{ {
if (IsOpen) _notificationNode.NotificationInfo = info; if (IsOpen) _notificationNode.NotificationInfo = info;
}, delayTicks: 1); }, delayTicks: 3);
} }
public void SetSearchText(string searchText) protected override void OnFinalize(AtkUnitBase* addon)
{
Services.Framework.RunOnTick(() =>
{
if(IsOpen) _searchInputNode.SearchString = searchText;
RefreshCategoriesCore(autosize: true);
}, delayTicks: 1);
}
protected override unsafe void OnFinalize(AtkUnitBase* addon)
{ {
ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId; ref var blockingAddonId = ref AgentInventoryContext.Instance()->BlockingAddonId;
if (blockingAddonId != 0) if (blockingAddonId != 0)
@@ -333,13 +101,9 @@ public class AddonInventoryWindow : NativeAddon
RaptureAtkModule.Instance()->CloseAddon(blockingAddonId); RaptureAtkModule.Instance()->CloseAddon(blockingAddonId);
} }
Services.AddonLifecycle.UnregisterListener(OnInventoryUpdate);
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory); addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
_hoverSubscribed.Clear(); _isSetupComplete = false;
_refreshQueued = false;
_refreshAutosizeQueued = false;
base.OnFinalize(addon); 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.Configuration;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using KamiToolKit.Premade; using KamiToolKit.Premade;
namespace AetherBags.Addons; 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 System;
using AetherBags.Helpers; using AetherBags.Helpers;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.State;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace AetherBags.Commands; 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 argsParts = args.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty; var subCommand = argsParts.Length > 0 ? argsParts[0].ToLowerInvariant() : string.Empty;
@@ -57,7 +59,7 @@ public class CommandHandler : IDisposable
break; break;
case "refresh": case "refresh":
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
PrintChat("Inventory refreshed."); PrintChat("Inventory refreshed.");
break; break;
@@ -67,7 +69,7 @@ public class CommandHandler : IDisposable
case "import-sk": case "import-sk":
ImportExportResetHelper.TryImportSortaKindaFromClipboard(true); ImportExportResetHelper.TryImportSortaKindaFromClipboard(true);
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "export": case "export":
@@ -76,12 +78,12 @@ public class CommandHandler : IDisposable
case "import": case "import":
ImportExportResetHelper.TryImportConfigFromClipboard(); ImportExportResetHelper.TryImportConfigFromClipboard();
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "reset": case "reset":
ImportExportResetHelper.TryResetConfig(); ImportExportResetHelper.TryResetConfig();
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
break; break;
case "count": 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"); PrintChat($"{stats.UsedSlots}/{stats.TotalSlots} slots used ({stats.UsagePercent:F0}%) | {stats.TotalItems} unique items | {stats.CategoryCount} categories");
break; break;
case "saddle":
System.AddonSaddleBagWindow.Toggle();
break;
case "retainer":
System.AddonRetainerWindow.Toggle();
break;
case "help": case "help":
case "?": case "?":
PrintHelp(); PrintHelp();
@@ -8,8 +8,12 @@ namespace AetherBags.Configuration;
public class CategorySettings public class CategorySettings
{ {
public bool CategoriesEnabled { get; set; } = true;
public bool GameCategoriesEnabled { get; set; } = true; public bool GameCategoriesEnabled { get; set; } = true;
public bool UserCategoriesEnabled { 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(); public List<UserCategoryDefinition> UserCategories { get; set; } = new();
} }
@@ -76,3 +80,9 @@ public enum ToggleFilterState
Allow = 1, Allow = 1,
Disallow = 2, Disallow = 2,
} }
public enum AllaganToolsFilterMode
{
Categorize = 0,
Highlight = 1,
}
@@ -3,6 +3,7 @@ namespace AetherBags.Configuration;
public class GeneralSettings public class GeneralSettings
{ {
public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId; public InventoryStackMode StackMode { get; set; } = InventoryStackMode.AggregateByItemId;
public SearchMode SearchMode { get; set; } = SearchMode.Highlight;
public bool DebugEnabled { get; set; } = false; public bool DebugEnabled { get; set; } = false;
public bool CompactPackingEnabled { get; set; } = true; public bool CompactPackingEnabled { get; set; } = true;
public int CompactLookahead { get; set; } = 24; public int CompactLookahead { get; set; } = 24;
@@ -10,6 +11,10 @@ public class GeneralSettings
public bool CompactStableInsert { get; set; } = true; public bool CompactStableInsert { get; set; } = true;
public bool OpenWithGameInventory { get; set; } = true; public bool OpenWithGameInventory { get; set; } = true;
public bool HideGameInventory { get; set; } = false; 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 ShowCategoryItemCount { get; set; } = false;
public bool LinkItemEnabled { get; set; } = false; public bool LinkItemEnabled { get; set; } = false;
} }
@@ -19,3 +24,9 @@ public enum InventoryStackMode : byte
NaturalStacks = 0, NaturalStacks = 0,
AggregateByItemId = 1, 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 AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -8,55 +8,8 @@ using Lumina.Text;
namespace AetherBags.Extensions; namespace AetherBags.Extensions;
// TODO: Remove FixedInterface when CS is merged into Dalamud.
public static unsafe class DragDropPayloadExtensions 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) extension(DragDropPayload payload)
{ {
public bool IsValidInventoryPayload => public bool IsValidInventoryPayload =>
@@ -65,6 +18,15 @@ public static unsafe class DragDropPayloadExtensions
or DragDropType.RemoteInventory_Item or DragDropType.RemoteInventory_Item
or DragDropType.Item; or DragDropType.Item;
public bool IsSameBaseContainer(DragDropPayload otherPayload) {
if (payload.InventoryLocation.Container.IsSameContainerGroup(otherPayload.InventoryLocation.Container))
{
return true;
}
return false;
}
public InventoryLocation InventoryLocation public InventoryLocation InventoryLocation
{ {
get get
@@ -115,6 +115,9 @@ public static unsafe class InventoryItemExtensions {
if (InventoryManager.Instance()->GetInventoryItemCount(itemId, true) > 0) if (InventoryManager.Instance()->GetInventoryItemCount(itemId, true) > 0)
itemId += 1_000_000; itemId += 1_000_000;
if (!item.Container.IsMainInventory)
return;
AgentInventoryContext.Instance()->UseItem(itemId, type); AgentInventoryContext.Instance()->UseItem(itemId, type);
} }
} }
@@ -1,6 +1,7 @@
using AetherBags.Inventory; using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Misc; using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using InventoryManager = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager;
namespace AetherBags.Extensions; namespace AetherBags.Extensions;
@@ -107,17 +108,17 @@ public static unsafe class InventoryTypeExtensions
}; };
public int GetInventoryStartIndex => inventoryType switch { public int GetInventoryStartIndex => inventoryType switch {
InventoryType.Inventory2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.Inventory2 => inventoryType.UIPageSize,
InventoryType.Inventory3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, InventoryType.Inventory3 => inventoryType.UIPageSize * 2,
InventoryType.Inventory4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, InventoryType.Inventory4 => inventoryType.UIPageSize * 3,
InventoryType.SaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.SaddleBag2 => inventoryType.UIPageSize,
InventoryType.PremiumSaddleBag2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.PremiumSaddleBag2 => inventoryType.UIPageSize,
InventoryType.RetainerPage2 => inventoryType.GetInventorySorter->ItemsPerPage, InventoryType.RetainerPage2 => inventoryType.UIPageSize,
InventoryType.RetainerPage3 => inventoryType.GetInventorySorter->ItemsPerPage * 2, InventoryType.RetainerPage3 => inventoryType.UIPageSize * 2,
InventoryType.RetainerPage4 => inventoryType.GetInventorySorter->ItemsPerPage * 3, InventoryType.RetainerPage4 => inventoryType.UIPageSize * 3,
InventoryType.RetainerPage5 => inventoryType.GetInventorySorter->ItemsPerPage * 4, InventoryType.RetainerPage5 => inventoryType.UIPageSize * 4,
InventoryType.RetainerPage6 => inventoryType.GetInventorySorter->ItemsPerPage * 5, InventoryType.RetainerPage6 => inventoryType.UIPageSize * 5,
InventoryType.RetainerPage7 => inventoryType.GetInventorySorter->ItemsPerPage * 6, InventoryType.RetainerPage7 => inventoryType.UIPageSize * 6,
_ => 0, _ => 0,
}; };
@@ -156,6 +157,14 @@ public static unsafe class InventoryTypeExtensions
InventoryType.RetainerPage6 or InventoryType.RetainerPage6 or
InventoryType.RetainerPage7; 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 public int ContainerGroup => inventoryType switch
{ {
_ when inventoryType.IsMainInventory => 1, _ when inventoryType.IsMainInventory => 1,
@@ -165,6 +174,8 @@ public static unsafe class InventoryTypeExtensions
_ => 0, _ => 0,
}; };
public bool IsLoaded => InventoryManager.Instance()->GetInventoryContainer(inventoryType)->IsLoaded;
public bool IsSameContainerGroup(InventoryType other) public bool IsSameContainerGroup(InventoryType other)
=> inventoryType.ContainerGroup == other.ContainerGroup; => inventoryType.ContainerGroup == other.ContainerGroup;
+8 -5
View File
@@ -2,13 +2,16 @@ namespace AetherBags.Extensions;
public static class LoggerExtensions 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) public void DebugOnly(string message, params object[] args) => DebugOnly(logger, string.Format(message, args));
{
if(System.Config.General.DebugEnabled) Services.Logger.Debug(message);
} }
} }
+1 -1
View File
@@ -14,7 +14,7 @@ public static class BackupHelper {
private const int MaxBackups = 10; private const int MaxBackups = 10;
private const string Name = "AetherBags"; private const string Name = "AetherBags";
public static void DoConfigBackup(IDalamudPluginInterface pluginInterface) { public static void DoConfigBackup(IDalamudPluginInterface pluginInterface) {
Services.Logger.Debug("Backup configuration start."); Services.Logger.DebugOnly("Backup configuration start.");
try { try {
var configDirectory = pluginInterface.ConfigDirectory; var configDirectory = pluginInterface.ConfigDirectory;
if (!configDirectory.Exists) { if (!configDirectory.Exists) {
+26 -19
View File
@@ -1,45 +1,52 @@
using System;
using FFXIVClientStructs.FFXIV.Client.Game; 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; namespace AetherBags. Helpers;
public static unsafe class InventoryMoveHelper 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) 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); InventoryManager.Instance()->MoveItemSlot(sourceContainer, sourceSlot, destContainer, destSlot, true);
Services.Framework.DelayTicks(2); Services.Framework.DelayTicks(3);
Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualInventoryRefresh); Services.Framework.RunOnFrameworkThread(System.AddonInventoryWindow.ManualRefresh);
} }
/* public static void HandleItemMovePayload(DragDropPayload source, DragDropPayload target)
private static void MoveItemViaAgent(InventoryType sourceInventory, ushort sourceSlot, InventoryType destInventory, ushort destSlot)
{ {
uint sourceContainerId = sourceInventory.AgentItemContainerId; uint srcContainer = (uint)source.Int1;
uint destContainerId = destInventory.AgentItemContainerId; uint dstContainer = (uint)target.Int1;
if (sourceContainerId == 0 || destContainerId == 0) uint srcSlot = (uint)source.Int2;
{ uint dstSlot = (uint)target.Int2;
Services.Logger.Warning($"[MoveItemViaAgent] Invalid container IDs: src={sourceContainerId}, dst={destContainerId}");
return;
}
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]; var atkValues = stackalloc AtkValue[4];
for (var i = 0; i < 4; i++) for (var i = 0; i < 4; i++)
{
atkValues[i].Type = ValueType.UInt; atkValues[i].Type = ValueType.UInt;
}
atkValues[0].SetUInt(sourceContainerId); atkValues[0].UInt = srcContainer;
atkValues[1].SetUInt(sourceSlot); atkValues[1].UInt = srcSlot;
atkValues[2].SetUInt(destContainerId); atkValues[2].UInt = dstContainer;
atkValues[3].SetUInt(destSlot); atkValues[3].UInt = dstSlot;
var retVal = stackalloc AtkValue[1]; var retVal = stackalloc AtkValue[1];
RaptureAtkModule* atkModule = RaptureAtkModule.Instance(); RaptureAtkModule* atkModule = RaptureAtkModule.Instance();
atkModule->HandleItemMove(retVal, atkValues, 4); atkModule->HandleItemMove(retVal, atkValues, 4);
} }
*/
} }
+11 -10
View File
@@ -37,7 +37,7 @@ public sealed unsafe class InventoryHooks : IDisposable
MoveItemSlotDetour); MoveItemSlotDetour);
_moveItemSlotHook.Enable(); _moveItemSlotHook.Enable();
Services.Logger.Debug("MoveItemSlot hooked successfully."); Services.Logger.DebugOnly("MoveItemSlot hooked successfully.");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -51,7 +51,7 @@ public sealed unsafe class InventoryHooks : IDisposable
OpenInventoryDetour); OpenInventoryDetour);
_openInventoryHook.Enable(); _openInventoryHook.Enable();
Services.Logger.Debug("OpenInventory hooked successfully."); Services.Logger.DebugOnly("OpenInventory hooked successfully.");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -64,7 +64,7 @@ public sealed unsafe class InventoryHooks : IDisposable
HandleInventoryEventDetour); HandleInventoryEventDetour);
_handleInventoryEventHook.Enable(); _handleInventoryEventHook.Enable();
Services.Logger.Debug("HandleInventoryEvent hooked successfully."); Services.Logger.DebugOnly("HandleInventoryEvent hooked successfully.");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -77,7 +77,7 @@ public sealed unsafe class InventoryHooks : IDisposable
OpenAddonDetour); OpenAddonDetour);
_openAddonHook.Enable(); _openAddonHook.Enable();
Services.Logger.Debug("OpenAddon hooked successfully."); Services.Logger.DebugOnly("OpenAddon hooked successfully.");
} }
catch (Exception e) catch (Exception e)
{ {
@@ -93,10 +93,11 @@ public sealed unsafe class InventoryHooks : IDisposable
ushort dstSlot, ushort dstSlot,
bool unk) bool unk)
{ {
InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot); //InventoryItem* sourceItem = InventoryManager.Instance()->GetInventorySlot(srcType, srcSlot);
InventoryItem* destItem = InventoryManager.Instance()->GetInventorySlot(dstType, dstSlot); //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); 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) 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); _openInventoryHook?.Original(uiModule, type);
} }
@@ -112,7 +113,7 @@ public sealed unsafe class InventoryHooks : IDisposable
{ {
for(int i = 0; i < valueCount; i++) 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); _handleInventoryEventHook?.Original(eventInterface, atkValue, valueCount);
} }
@@ -121,7 +122,7 @@ public sealed unsafe class InventoryHooks : IDisposable
{ {
for(int i = 0; i < valueCount; i++) 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); 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 System.Collections.Generic;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Categories;
public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List<ItemInfo> Items); public readonly record struct CategorizedInventory(uint Key, CategoryInfo Category, List<ItemInfo> Items);
@@ -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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
using KamiToolKit.Classes;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Categories;
public static class CategoryBucketManager public static class CategoryBucketManager
{ {
@@ -17,6 +19,15 @@ public static class CategoryBucketManager
public static bool IsUserCategoryKey(uint key) public static bool IsUserCategoryKey(uint key)
=> (key & UserCategoryKeyFlag) != 0; => (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> /// <summary>
/// Resets all buckets for a new refresh cycle. /// Resets all buckets for a new refresh cycle.
/// </summary> /// </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( public static void BucketUnclaimedToMisc(
Dictionary<ulong, ItemInfo> itemInfoByKey, Dictionary<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey, Dictionary<uint, CategoryBucket> bucketsByKey,
@@ -214,9 +293,13 @@ public static class CategoryBucketManager
sortedCategoryKeys.Sort((left, right) => sortedCategoryKeys.Sort((left, right) =>
{ {
bool leftCategory = IsUserCategoryKey(left); bool leftUser = IsUserCategoryKey(left);
bool rightCategory = IsUserCategoryKey(right); bool rightUser = IsUserCategoryKey(right);
if (leftCategory != rightCategory) return leftCategory ? -1 : 1; 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); 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 System.Numerics;
using KamiToolKit.Classes; using KamiToolKit.Classes;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Categories;
public class CategoryInfo public class CategoryInfo
{ {
@@ -1,8 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Categories;
public static class InventoryFilter public static class InventoryFilter
{ {
@@ -1,8 +1,9 @@
using AetherBags.Configuration;
using System; using System;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Categories;
internal static class UserCategoryMatcher internal static class UserCategoryMatcher
{ {
@@ -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.Excel.Sheets;
using Lumina.Text.ReadOnly; using Lumina.Text.ReadOnly;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Context;
public class InventoryNotificationState 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 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; 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 public readonly struct InventoryStats
{ {
@@ -1,11 +1,12 @@
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
using System; using System;
using System.Numerics; using System.Numerics;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
using Lumina.Excel.Sheets;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Items;
public sealed class ItemInfo : IEquatable<ItemInfo> public sealed class ItemInfo : IEquatable<ItemInfo>
{ {
@@ -57,15 +58,13 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality); public bool IsHq => Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality);
public bool IsDesynthesizable => Row.Desynth > 0; 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 IsGlamourable => Row.IsGlamorous;
public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000 public bool IsSpiritbonded => Item.SpiritbondOrCollectability >= 10000; // 100% = 10000
private string Description => _description ??= Row.Description.ToString(); private string Description => _description ??= Row.Description.ToString();
public InventoryMappedLocation VisualLocation => public InventoryMappedLocation VisualLocation => InventoryContextState.GetVisualLocation(Item.Container, Item.Slot);
IsMainInventory ? InventoryContextState.GetVisualLocation(InventoryPage, Item.Slot)
: new InventoryMappedLocation((int)Item.Container.AgentItemContainerId, Item. Slot);
public int InventoryPage => Item.Container switch public int InventoryPage => Item.Container switch
@@ -83,13 +82,55 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
{ {
get get
{ {
if (!InventoryContextState.HasActiveContext) if (IsSlotBlocked) return false;
return true; 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 IsMainInventory => InventoryPage >= 0;
public bool IsRegexMatch(string searchTerms) public bool IsRegexMatch(string searchTerms)
@@ -1,5 +1,5 @@
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Items;
public record LootedItemInfo(int Index, InventoryItem Item, int Quantity); public record LootedItemInfo(int Index, InventoryItem Item, int Quantity);
@@ -0,0 +1,9 @@
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory.Scanning;
public struct AggregatedItem
{
public InventoryItem First;
public int Total;
}
@@ -1,19 +1,12 @@
using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Client.Game;
using System.Collections.Generic; using System.Collections.Generic;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.Scanning;
public static unsafe class InventoryScanner public static unsafe class InventoryScanner
{ {
private static readonly InventoryType[] BagInventories =
[
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4,
];
public static readonly InventoryType[] StandardInventories = public static readonly InventoryType[] StandardInventories =
[ [
InventoryType.Inventory1, InventoryType.Inventory1,
@@ -46,20 +39,23 @@ public static unsafe class InventoryScanner
public static ulong MakeNaturalSlotKey(InventoryType container, int slot) public static ulong MakeNaturalSlotKey(InventoryType container, int slot)
=> ((ulong)(uint)container << 32) | (uint)slot; => ((ulong)(uint)container << 32) | (uint)slot;
public static void ScanBags( public static void ScanInventories(
InventoryManager* inventoryManager, InventoryManager* inventoryManager,
InventoryStackMode stackMode, InventoryStackMode stackMode,
Dictionary<ulong, AggregatedItem> aggByKey) Dictionary<ulong, AggregatedItem> aggByKey,
InventorySourceType source)
{ {
aggByKey.Clear(); aggByKey.Clear();
var inventories = InventorySourceDefinitions.GetInventories(source);
int scannedSlots = 0; int scannedSlots = 0;
int nonEmptySlots = 0; int nonEmptySlots = 0;
int collisions = 0; int collisions = 0;
for (int inventoryIndex = 0; inventoryIndex < BagInventories.Length; inventoryIndex++) for (int inventoryIndex = 0; inventoryIndex < inventories.Length; inventoryIndex++)
{ {
var inventoryType = BagInventories[inventoryIndex]; var inventoryType = inventories[inventoryIndex];
var container = inventoryManager->GetInventoryContainer(inventoryType); var container = inventoryManager->GetInventoryContainer(inventoryType);
if (container == null) if (container == null)
{ {
@@ -164,16 +160,58 @@ public static unsafe class InventoryScanner
public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType) public static InventoryContainer* GetInventoryContainer(InventoryType inventoryType)
=> InventoryManager.Instance()->GetInventoryContainer(inventoryType); => InventoryManager.Instance()->GetInventoryContainer(inventoryType);
public static string GetEmptyItemSlotsString() public static InventoryLocation GetFirstEmptySlot(InventorySourceType source)
{ {
uint empty = InventoryManager.Instance()->GetEmptySlotsInBag(); var manager = InventoryManager.Instance();
uint used = 140 - empty; var containers = InventorySourceDefinitions.GetContainersForSource(source);
return $"{used}/140";
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 return InventoryLocation.Invalid;
{ }
public InventoryItem First;
public int Total; 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.Configuration;
using AetherBags.Currency; using AetherBags.Currency;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items;
using AetherBags.Inventory.Scanning;
using Dalamud.Game.Inventory; using Dalamud.Game.Inventory;
using Dalamud.Game.Inventory.InventoryEventArgTypes; using Dalamud.Game.Inventory.InventoryEventArgTypes;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using System.Collections.Generic;
using System.Linq;
namespace AetherBags.Inventory; namespace AetherBags.Inventory.State;
public static unsafe class InventoryState public static unsafe class InventoryState
{ {
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, AggregatedItem> AggByKey = new(capacity: 512);
private static readonly Dictionary<ulong, ItemInfo> ItemInfoByKey = 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) public static bool Contains(this IReadOnlyCollection<InventoryType> inventoryTypes, GameInventoryType type)
=> inventoryTypes.Contains((InventoryType)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) public static IReadOnlyList<CategorizedInventory> GetInventoryItemCategories(string filterString = "", bool invert = false)
{ {
return InventoryFilter.FilterCategories( return InventoryFilter.FilterCategories(
@@ -110,7 +51,7 @@ public static unsafe class InventoryState
totalQuantity += kvp.Value.ItemCount; totalQuantity += kvp.Value.ItemCount;
} }
uint emptySlots = InventoryManager.Instance()->GetEmptySlotsInBag(); uint emptySlots = FFXIVClientStructs.FFXIV.Client.Game.InventoryManager.Instance()->GetEmptySlotsInBag();
const int totalSlots = 140; const int totalSlots = 140;
var categories = GetInventoryItemCategories(string.Empty); 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) public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
=> CurrencyState.GetCurrencyInfoList(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; if (_colorPickerAddon is not null) return;
_colorPickerAddon = new ColorPickerAddon { _colorPickerAddon = new ColorPickerAddon {
InternalName = "ColorPicker", InternalName = "ColorPicker_AetherBags",
Title = "ColorPicker_AetherBags", Title = "Pick a color",
}; };
} }
@@ -94,4 +94,5 @@ public class ColorInputRow : HorizontalListNode
public Action<Vector4>? OnColorConfirmed { get; set; } public Action<Vector4>? OnColorConfirmed { get; set; }
public Action<Vector4>? OnColorCanceled { get; set; } public Action<Vector4>? OnColorCanceled { get; set; }
public Action<Vector4>? OnColorChange { 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> public class CategoryConfigurationNode : ConfigNode<CategoryWrapper>
{ {
private readonly ScrollingAreaNode<VerticalListNode> _categoryList;
private CategoryDefinitionConfigurationNode? _activeNode; private CategoryDefinitionConfigurationNode? _activeNode;
public Action? OnCategoryChanged { get; set; } public Action? OnCategoryChanged { get; set; }
public CategoryConfigurationNode() public CategoryConfigurationNode()
{ {
_categoryList = new ScrollingAreaNode<VerticalListNode>
{
ContentHeight = 100.0f,
AutoHideScrollBar = true,
};
_categoryList.ContentNode.FitContents = true;
_categoryList.AttachNode(this);
} }
protected override void OptionChanged(CategoryWrapper? option) protected override void OptionChanged(CategoryWrapper? option)
{ {
if (option?.CategoryDefinition is null) if (option?.CategoryDefinition is null)
{ {
_categoryList.IsVisible = false; if (_activeNode is not null)
{
_activeNode.IsVisible = false;
}
return; return;
} }
_categoryList.IsVisible = true;
if (_activeNode is null) if (_activeNode is null)
{ {
_activeNode = new CategoryDefinitionConfigurationNode(option.CategoryDefinition) _activeNode = new CategoryDefinitionConfigurationNode
{ {
Size = _categoryList.ContentNode.Size, OnLayoutChanged = RecalculateLayout,
OnLayoutChanged = UpdateScrollHeight,
OnCategoryPropertyChanged = OnCategoryChanged, OnCategoryPropertyChanged = OnCategoryChanged,
}; };
_categoryList.ContentNode.AddNode(_activeNode); _activeNode.AttachNode(this);
} }
else
{ _activeNode.IsVisible = true;
_activeNode.Size = Size;
_activeNode.SetCategory(option.CategoryDefinition); _activeNode.SetCategory(option.CategoryDefinition);
} }
UpdateScrollHeight(); private void RecalculateLayout()
}
private void UpdateScrollHeight()
{ {
_categoryList.ContentNode.RecalculateLayout(); // Trigger parent layout update if needed
_categoryList.ContentHeight = _categoryList.ContentNode.Height;
} }
protected override void OnSizeChanged() protected override void OnSizeChanged()
{ {
base.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; _activeNode.Size = Size;
} }
UpdateScrollHeight();
} }
} }
@@ -1,7 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Nodes.Color; using AetherBags.Nodes.Color;
using Dalamud.Utility; using Dalamud.Utility;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@@ -14,515 +16,500 @@ using Action = System.Action;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class CategoryDefinitionConfigurationNode : VerticalListNode public sealed class CategoryDefinitionConfigurationNode : SimpleComponentNode
{ {
private readonly CheckboxNode _enabledCheckbox; private static ExcelSheet<Item>? ItemSheet => Services.DataManager.GetExcelSheet<Item>();
private readonly CheckboxNode _pinnedCheckbox; private static ExcelSheet<ItemUICategory>? UICategorySheet => Services.DataManager.GetExcelSheet<ItemUICategory>();
private readonly TextInputNode _nameInputNode;
private readonly TextInputNode _descriptionInputNode;
private readonly ColorInputRow _colorInputNode;
private readonly NumericInputNode _priorityInputNode;
private readonly NumericInputNode _orderInputNode;
private readonly CheckboxNode _levelEnabledCheckbox; public Action? OnLayoutChanged { get; init; }
private readonly NumericInputNode _levelMinNode; public Action? OnCategoryPropertyChanged { get; init; }
private readonly NumericInputNode _levelMaxNode;
private readonly CheckboxNode _itemLevelEnabledCheckbox; private UserCategoryDefinition _categoryDefinition = new();
private readonly NumericInputNode _itemLevelMinNode;
private readonly NumericInputNode _itemLevelMaxNode;
private readonly CheckboxNode _vendorPriceEnabledCheckbox; private readonly ScrollingAreaNode<TreeListNode> _scrollingArea;
private readonly NumericInputNode _vendorPriceMinNode; private readonly BasicSettingsSection _basicSettings;
private readonly NumericInputNode _vendorPriceMaxNode; private readonly RangeFiltersSection _rangeFilters;
private readonly StateFiltersSection _stateFilters;
private readonly ListFiltersSection _listFilters;
private readonly StateFilterRowNode _untradableFilter; public CategoryDefinitionConfigurationNode()
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)
{ {
CategoryDefinition = categoryDefinition; _scrollingArea = new ScrollingAreaNode<TreeListNode>
_sItemSheet ??= Services.DataManager.GetExcelSheet<Item>();
_sUICategorySheet ??= Services.DataManager.GetExcelSheet<ItemUICategory>();
FitContents = true;
ItemSpacing = 4.0f;
var catchAllWarningNode = new TextNode
{ {
Size = new Vector2(300, 40), ContentHeight = 100.0f,
TextFlags = TextFlags.MultiLine | TextFlags.AutoAdjustNodeSize, AutoHideScrollBar = true,
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),
}; };
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 _enabledCheckbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(Width, 20),
String = "Enabled", String = "Enabled",
IsChecked = CategoryDefinition.Enabled,
OnClick = isChecked => OnClick = isChecked =>
{ {
CategoryDefinition.Enabled = isChecked; CategoryDefinition.Enabled = isChecked;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_enabledCheckbox); AddNode(_enabledCheckbox);
_pinnedCheckbox = new CheckboxNode _pinnedCheckbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(Width, 20),
String = "Pinned", String = "Pinned",
IsChecked = CategoryDefinition.Pinned,
OnClick = isChecked => OnClick = isChecked =>
{ {
CategoryDefinition.Pinned = isChecked; CategoryDefinition.Pinned = isChecked;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_pinnedCheckbox); AddNode(_pinnedCheckbox);
AddNode(new LabelTextNode AddNode(CreateLabel("Name: "));
{ _nameInput = new TextInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Name:"
});
_nameInputNode = new TextInputNode
{ {
Size = new Vector2(250, 28), Size = new Vector2(250, 28),
String = CategoryDefinition.Name, PlaceholderString = "Category Name",
PlaceholderString = CategoryDefinition.Name.IsNullOrWhitespace() ? "Category Name" : "", OnInputReceived = input =>
OnInputReceived = name =>
{ {
CategoryDefinition.Name = name.ExtractText(); CategoryDefinition.Name = input.ExtractText();
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_nameInputNode); AddNode(_nameInput);
AddNode(new LabelTextNode AddNode(CreateLabel("Description:"));
{ _descriptionInput = new TextInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Description:"
});
_descriptionInputNode = new TextInputNode
{ {
Size = new Vector2(250, 28), Size = new Vector2(250, 28),
String = CategoryDefinition.Description, PlaceholderString = "Optional description",
PlaceholderString = CategoryDefinition.Description.IsNullOrWhitespace() ? "Optional description" : "", OnInputReceived = input =>
OnInputReceived = desc =>
{ {
CategoryDefinition.Description = desc.ExtractText(); CategoryDefinition.Description = input.ExtractText();
NotifyChanged(); OnValueChanged?.Invoke();
}, },
}; };
AddNode(_descriptionInputNode); AddNode(_descriptionInput);
_colorInputNode = new ColorInputRow _colorInput = new ColorInputRow
{ {
Label = "Color", Label = "Color",
Size = new Vector2(300, 28), Size = new Vector2(300, 28),
CurrentColor = CategoryDefinition.Color, CurrentColor = new UserCategoryDefinition().Color,
DefaultColor = new UserCategoryDefinition().Color, DefaultColor = new UserCategoryDefinition().Color,
OnColorConfirmed = color => OnColorConfirmed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
{ OnColorCanceled = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
CategoryDefinition.Color = color; OnColorPreviewed = c => { CategoryDefinition.Color = c; OnValueChanged?.Invoke(); },
NotifyChanged();
},
OnColorCanceled = color =>
{
CategoryDefinition.Color = color;
NotifyChanged();
},
}; };
AddNode(_colorInputNode); AddNode(_colorInput);
AddNode(new LabelTextNode AddNode(CreateLabel("Priority:"));
{ _priorityInput = new NumericInputNode
TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(80, 20),
String = "Priority:"
});
_priorityInputNode = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, 28),
Min = 0, Min = 0,
Max = 1000, Max = 1000,
Step = 1, Step = 1,
Value = CategoryDefinition.Priority,
OnValueUpdate = val => OnValueUpdate = val =>
{ {
CategoryDefinition.Priority = val; CategoryDefinition.Priority = val;
NotifyChanged(); OnValueChanged?.Invoke();
}, },
}; };
AddNode(_priorityInputNode); AddNode(_priorityInput);
AddNode(new LabelTextNode { TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(80, 20), String = "Order:" }); AddNode(CreateLabel("Order: "));
_orderInputNode = new NumericInputNode _orderInput = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, 28),
Min = 0, Min = 0,
Max = 9999, Max = 9999,
Step = 1, Step = 1,
Value = CategoryDefinition.Order,
OnValueUpdate = val => OnValueUpdate = val =>
{ {
CategoryDefinition.Order = val; CategoryDefinition.Order = val;
NotifyChanged(); OnPropertyChanged?.Invoke();
NotifyCategoryPropertyChanged();
}, },
}; };
AddNode(_orderInputNode); AddNode(_orderInput);
AddNode(CreateSectionHeader("Range Filters")); RecalculateLayout();
}
(_levelEnabledCheckbox, _levelMinNode, _levelMaxNode) = CreateRangeFilter( public void Refresh()
"Level", {
CategoryDefinition.Rules.Level, EnsureInitialized();
0, 200,
(enabled, min, max) => _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.Enabled = enabled;
CategoryDefinition.Rules.Level.Min = min; CategoryDefinition.Rules.Level.Min = min;
CategoryDefinition.Rules.Level.Max = max; CategoryDefinition.Rules.Level.Max = max;
NotifyChanged(); OnValueChanged?.Invoke();
} },
); };
AddNode(_levelFilter);
(_itemLevelEnabledCheckbox, _itemLevelMinNode, _itemLevelMaxNode) = CreateRangeFilter( _itemLevelFilter = new RangeFilterRow
"Item Level", {
CategoryDefinition.Rules.ItemLevel, Label = "Item Level",
0, 2000, MinBound = 0,
(enabled, min, max) => MaxBound = 2000,
OnFilterChanged = (enabled, min, max) =>
{ {
CategoryDefinition.Rules.ItemLevel.Enabled = enabled; CategoryDefinition.Rules.ItemLevel.Enabled = enabled;
CategoryDefinition.Rules.ItemLevel.Min = min; CategoryDefinition.Rules.ItemLevel.Min = min;
CategoryDefinition.Rules.ItemLevel.Max = max; CategoryDefinition.Rules.ItemLevel.Max = max;
NotifyChanged(); OnValueChanged?.Invoke();
} },
);
(_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),
}; };
} AddNode(_itemLevelFilter);
private (CheckboxNode enabled, NumericInputNode min, NumericInputNode max) CreateRangeFilter( _vendorPriceFilter = new RangeFilterRowUint
string label,
RangeFilter<int> filter,
int minBound,
int maxBound,
Action<bool, int, int> onUpdate)
{ {
var enabledCheckbox = new CheckboxNode Label = "Vendor Price",
MinBound = 0,
MaxBound = 9_999_999,
OnFilterChanged = (enabled, min, max) =>
{ {
Size = new Vector2(200, 20), CategoryDefinition.Rules.VendorPrice.Enabled = enabled;
String = $"{label} Filter", CategoryDefinition.Rules.VendorPrice.Min = min;
IsChecked = filter.Enabled, CategoryDefinition.Rules.VendorPrice.Max = max;
OnValueChanged?.Invoke();
},
}; };
AddNode(enabledCheckbox); AddNode(_vendorPriceFilter);
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);
RecalculateLayout(); RecalculateLayout();
OnLayoutChanged?.Invoke();
} }
private static void RefreshRangeFilter(CheckboxNode enabled, NumericInputNode min, NumericInputNode max, RangeFilter<int> filter) public void Refresh()
{ {
enabled.IsChecked = filter.Enabled; EnsureInitialized();
min.Value = filter.Min;
max.Value = filter.Max; _levelFilter!.SetFilter(CategoryDefinition.Rules.Level);
min.IsEnabled = filter.Enabled; _itemLevelFilter!.SetFilter(CategoryDefinition.Rules.ItemLevel);
max.IsEnabled = filter.Enabled; _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; namespace AetherBags.Nodes.Configuration.Category;
public class CategoryScrollingAreaNode : ScrollingAreaNode<VerticalListNode> public sealed class CategoryScrollingAreaNode : ScrollingListNode
{ {
private AddonCategoryConfigurationWindow? _categoryConfigurationAddon; private AddonCategoryConfigurationWindow? _categoryConfigurationAddon;
private readonly TextButtonNode _categoryConfigurationButtonNode; private readonly TextButtonNode _categoryConfigurationButtonNode;
@@ -13,13 +13,15 @@ public class CategoryScrollingAreaNode : ScrollingAreaNode<VerticalListNode>
{ {
InitializeCategoryAddon(); InitializeCategoryAddon();
AddNode(new CategoryGeneralConfigurationNode());
_categoryConfigurationButtonNode = new TextButtonNode _categoryConfigurationButtonNode = new TextButtonNode
{ {
Size = new Vector2(300, 28), Size = new Vector2(300, 28),
String = "Configure Categories", String = "Configure Categories",
OnClick = () => _categoryConfigurationAddon?.Toggle(), OnClick = () => _categoryConfigurationAddon?.Toggle(),
}; };
_categoryConfigurationButtonNode.AttachNode(this); AddNode(_categoryConfigurationButtonNode);
} }
private void InitializeCategoryAddon() { 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class RarityEditorNode :VerticalListNode public sealed class RarityEditorNode :VerticalListNode
{ {
private static readonly string[] RarityNames = { "Common (White)", "Uncommon (Green)", "Rare (Blue)", "Relic (Purple)", "Aetherial (Pink)" }; private const float LabelWidth = 120f;
private const float CheckboxWidth = 150f;
private List<int> _list; private static readonly string[] RarityNames =
private readonly List<CheckboxNode> _checkboxes = new(); [
private readonly Action? _onChanged; "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; FitContents = true;
ItemSpacing = 2.0f; ItemSpacing = 2.0f;
@@ -32,15 +40,21 @@ public sealed class RarityEditorNode : VerticalListNode
}; };
AddNode(headerLabel); AddNode(headerLabel);
for (int i = 0; i < RarityNames.Length; i++) for (var i = 0; i < RarityNames.Length; i++)
{ {
var rarity = i; var rarity = i;
var checkbox = new CheckboxNode var checkbox = new CheckboxNode
{ {
Size = new Vector2(200, 20), Size = new Vector2(LabelWidth + CheckboxWidth, 22),
String = RarityNames[i], String = RarityNames[i],
IsChecked = _list.Contains(i), OnClick = isChecked => ToggleRarity(rarity, isChecked),
OnClick = isChecked => };
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
}
private void ToggleRarity(int rarity, bool isChecked)
{ {
if (isChecked && !_list.Contains(rarity)) if (isChecked && !_list.Contains(rarity))
{ {
@@ -51,12 +65,8 @@ public sealed class RarityEditorNode : VerticalListNode
{ {
_list.Remove(rarity); _list.Remove(rarity);
} }
_onChanged?.Invoke();
}, OnChanged?.Invoke();
};
_checkboxes.Add(checkbox);
AddNode(checkbox);
}
} }
public void SetList(List<int> newList) public void SetList(List<int> newList)
@@ -67,7 +77,7 @@ public sealed class RarityEditorNode : VerticalListNode
public void Refresh() 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); _checkboxes[i].IsChecked = _list.Contains(i);
} }
@@ -2,6 +2,7 @@ using AetherBags.Configuration;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using KamiToolKit.Premade.Nodes;
using System; using System;
using System.Numerics; using System.Numerics;
@@ -9,48 +10,54 @@ namespace AetherBags.Nodes.Configuration.Category;
public sealed class StateFilterRowNode : HorizontalListNode public sealed class StateFilterRowNode : HorizontalListNode
{ {
private readonly LabelTextNode _labelNode; private const float LabelWidth = 120f;
private readonly TextButtonNode _stateButton; private const float ButtonWidth = 100f;
private readonly StateFilterButton _stateButton;
private readonly Action? _onChanged; private readonly Action? _onChanged;
private StateFilter _filter; private StateFilter _filter;
private static readonly string[] StateLabels = { "Ignored", "Allow", "Disallow" };
public StateFilterRowNode(string label, StateFilter filter, Action?onChanged = null) public StateFilterRowNode(string label, StateFilter filter, Action?onChanged = null)
{ {
_filter = filter; _filter = filter;
_onChanged = onChanged; _onChanged = onChanged;
Size = new Vector2(280, 24); Size = new Vector2(LabelWidth + ButtonWidth + 8f, 24);
ItemSpacing = 8.0f; ItemSpacing = 8.0f;
_labelNode = new LabelTextNode var labelNode = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(LabelWidth, 24),
Size = new Vector2(100, 24),
String = $"{label}:", String = $"{label}:",
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
AlignmentType = AlignmentType.Right,
}; };
AddNode(_labelNode); AddNode(labelNode);
_stateButton = new TextButtonNode _stateButton = new StateFilterButton
{ {
Size = new Vector2(100, 24), Size = new Vector2(ButtonWidth, 24),
String = StateLabels[_filter.State], States = [0, 1, 2],
OnClick = CycleState, SelectedState = _filter.State,
OnStateChanged = newState =>
{
_filter.State = newState;
_onChanged?.Invoke();
}
}; };
AddNode(_stateButton); AddNode(_stateButton);
} }
private void CycleState()
{
_filter.State = (_filter.State + 1) % 3;
_stateButton.String = StateLabels[_filter.State];
_onChanged?.Invoke();
}
public void SetState(StateFilter newFilter) public void SetState(StateFilter newFilter)
{ {
_filter = 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;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class StringListEditorNode : VerticalListNode public sealed class StringListEditorNode : VerticalListNode
{ {
private List<string> _list; private const float LabelWidth = 300f;
private readonly TextInputNode _addInput; private const float RowHeight = 28f;
private List<string> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer; 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; get => _headerLabel.String;
_onChanged = onChanged; init => _headerLabel.String = value;
}
public StringListEditorNode()
{
FitContents = true; FitContents = true;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode _headerLabel = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18), Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
}; };
AddNode(headerLabel); AddNode(_headerLabel);
_itemsContainer = new VerticalListNode _itemsContainer = new VerticalListNode
{ {
FitContents = true, Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f, ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
}; };
AddNode(_itemsContainer); 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 _addInput = new TextInputNode
{ {
Size = new Vector2(200, 28), Size = new Vector2(200, RowHeight),
PlaceholderString = "Add new...", PlaceholderString = "Add new...",
OnInputComplete = text => OnInputComplete = _ => AddCurrentValue(),
{
var value = text.ExtractText();
if (!string.IsNullOrWhiteSpace(value) && ! _list.Contains(value))
{
_list.Add(value);
_addInput?.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(_addInput); _addRow.AddNode(_addInput);
var addButton = new TextButtonNode var addButton = new TextButtonNode
{ {
Size = new Vector2(60, 28), Size = new Vector2(60, RowHeight),
String = "Add", String = "Add",
OnClick = () => OnClick = AddCurrentValue,
{
var value = _addInput.String;
if (!string.IsNullOrWhiteSpace(value) && !_list.Contains(value))
{
_list.Add(value);
_addInput.String = "";
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(addButton); _addRow.AddNode(addButton);
AddNode(addRow); AddNode(_addRow);
RefreshItems();
} }
public void SetList(List<string> newList) public void SetList(List<string> newList)
@@ -87,35 +80,54 @@ public sealed class StringListEditorNode : VerticalListNode
RefreshItems(); 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() private void RefreshItems()
{ {
_itemsContainer.SyncWithListData( _itemsContainer.Clear();
_list,
node => node.Value, foreach (var value in _list)
value => new StringListItemNode(value)
{ {
Size = new Vector2(280, 22), _itemsContainer.AddNode(CreateItemNode(value));
OnRemove = () => }
{
_list.Remove(value); if (_list.Count == 0)
RefreshItems(); {
_onChanged?.Invoke(); _itemsContainer.Height = 0;
},
} }
);
_itemsContainer.RecalculateLayout(); _itemsContainer.RecalculateLayout();
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(); RefreshItems();
OnChanged?.Invoke();
} }
} }
public sealed class StringListItemNode : HorizontalListNode public sealed class StringListItemNode : HorizontalListNode
{ {
private const float LabelWidth = 300f;
public string Value { get; } public string Value { get; }
public Action? OnRemove { get; init; } public Action? OnRemove { get; init; }
@@ -124,20 +136,18 @@ public sealed class StringListItemNode : HorizontalListNode
Value = value; Value = value;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var itemLabel = new LabelTextNode AddNode(new LabelTextNode
{ {
Size = new Vector2(220, 22), Size = new Vector2(LabelWidth, 24),
String = value, String = value,
TextColor = ColorHelper.GetColor(3), TextColor = ColorHelper.GetColor(3),
}; });
AddNode(itemLabel);
var removeButton = new TextButtonNode AddNode(new CircleButtonNode
{ {
Size = new Vector2(50, 22), Size = new Vector2(28, 28),
String = "X", Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(), OnClick = () => OnRemove?.Invoke(),
}; });
AddNode(removeButton);
} }
} }
@@ -1,76 +1,79 @@
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes;
using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Category; namespace AetherBags.Nodes.Configuration.Category;
public sealed class UintListEditorNode : VerticalListNode public sealed class UintListEditorNode : VerticalListNode
{ {
private List<uint> _list; private const float LabelWidth = 300f;
private readonly NumericInputNode _addInput; private const float RowHeight = 28f;
private List<uint> _list = [];
private readonly LabelTextNode _headerLabel;
private readonly VerticalListNode _itemsContainer; private readonly VerticalListNode _itemsContainer;
private readonly Action? _onChanged; private readonly HorizontalListNode _addRow;
private readonly Func<uint, string>? _labelResolver; 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; get => _headerLabel.String;
_onChanged = onChanged; init => _headerLabel.String = value;
_labelResolver = labelResolver; }
public UintListEditorNode()
{
FitContents = true; FitContents = true;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var headerLabel = new LabelTextNode _headerLabel = new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, TextFlags = TextFlags.AutoAdjustNodeSize,
Size = new Vector2(280, 18), Size = new Vector2(280, 18),
String = label,
TextColor = ColorHelper.GetColor(8), TextColor = ColorHelper.GetColor(8),
}; };
AddNode(headerLabel); AddNode(_headerLabel);
_itemsContainer = new VerticalListNode _itemsContainer = new VerticalListNode
{ {
FitContents = true, Size = new Vector2(LabelWidth + 40f, 0),
ItemSpacing = 2.0f, ItemSpacing = 2.0f,
FitContents = true,
FirstItemSpacing = 2,
}; };
AddNode(_itemsContainer); 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 _addInput = new NumericInputNode
{ {
Size = new Vector2(120, 28), Size = new Vector2(120, RowHeight),
Min = 0, Min = 0,
Max = int.MaxValue, Max = int.MaxValue,
Value = 0, Value = 0,
}; };
addRow.AddNode(_addInput); _addRow.AddNode(_addInput);
var addButton = new TextButtonNode var addButton = new TextButtonNode
{ {
Size = new Vector2(60, 28), Size = new Vector2(60, RowHeight),
String = "Add", String = "Add",
OnClick = () => OnClick = AddCurrentValue,
{
var value = (uint)_addInput.Value;
if (! _list.Contains(value))
{
_list.Add(value);
RefreshItems();
_onChanged?.Invoke();
}
},
}; };
addRow.AddNode(addButton); _addRow.AddNode(addButton);
AddNode(addRow); AddNode(_addRow);
RefreshItems();
} }
public void SetList(List<uint> newList) public void SetList(List<uint> newList)
@@ -79,35 +82,53 @@ public sealed class UintListEditorNode : VerticalListNode
RefreshItems(); RefreshItems();
} }
private void AddCurrentValue()
{
var value = (uint)_addInput.Value;
if (!_list.Contains(value))
{
_list.Add(value);
RefreshItems();
OnChanged?.Invoke();
}
}
private void RefreshItems() private void RefreshItems()
{ {
_itemsContainer.SyncWithListData( _itemsContainer.Clear();
_list,
node => node.Value, foreach (var value in _list)
value => new UintListItemNode(value, _labelResolver)
{ {
Size = new Vector2(280, 22), _itemsContainer.AddNode(CreateItemNode(value));
OnRemove = () => }
{
_list.Remove(value); if (_list.Count == 0)
RefreshItems(); {
_onChanged?.Invoke(); _itemsContainer.Height = 0;
},
} }
);
_itemsContainer.RecalculateLayout(); _itemsContainer.RecalculateLayout();
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(); RefreshItems();
OnChanged?.Invoke();
} }
} }
public sealed class UintListItemNode : HorizontalListNode public sealed class UintListItemNode : HorizontalListNode
{ {
private const float LabelWidth = 300f;
public uint Value { get; } public uint Value { get; }
public Action? OnRemove { get; init; } public Action? OnRemove { get; init; }
@@ -116,22 +137,22 @@ public sealed class UintListItemNode : HorizontalListNode
Value = value; Value = value;
ItemSpacing = 4.0f; ItemSpacing = 4.0f;
var displayText = labelResolver != null ? $"{value} - {labelResolver(value)}" : value.ToString(); var displayText = labelResolver is not null
var itemLabel = new LabelTextNode ? $"{value} - {labelResolver(value)}"
: value.ToString();
AddNode(new LabelTextNode
{ {
TextFlags = TextFlags.AutoAdjustNodeSize, Size = new Vector2(LabelWidth, 24),
Size = new Vector2(220, 22),
String = displayText, String = displayText,
TextColor = ColorHelper.GetColor(3), TextColor = ColorHelper.GetColor(3),
}; });
AddNode(itemLabel);
var removeButton = new TextButtonNode AddNode(new CircleButtonNode
{ {
Size = new Vector2(50, 22), Size = new Vector2(28, 28),
String = "X", Icon = ButtonIcon.Cross,
OnClick = () => OnRemove?.Invoke(), 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; CurrencySettings config = System.Config.Currency;
ItemVerticalSpacing = 2;
LabelTextNode titleNode = new LabelTextNode LabelTextNode titleNode = new LabelTextNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
@@ -51,14 +53,13 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
}; };
AddNode(defaultCurrencyColorNode); AddNode(defaultCurrencyColorNode);
AddNode();
CheckboxNode cappedEnabledCheckbox = new CheckboxNode CheckboxNode cappedEnabledCheckbox = new CheckboxNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
IsVisible = true, IsVisible = true,
String = "Color When Capped", String = "Color Weekly Cap",
IsChecked = config.ColorWhenCapped, 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 => OnClick = isChecked =>
{ {
config.ColorWhenCapped = isChecked; config.ColorWhenCapped = isChecked;
@@ -69,9 +70,10 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
AddTab(1); AddTab(1);
ColorInputRow cappedCurrencyColorNode = new ColorInputRow ColorInputRow cappedCurrencyColorNode = new ColorInputRow
{ {
Label = "Capped Currency Color", Label = "Weekly Cap Color",
Size = new Vector2(300, 24), Size = new Vector2(300, 24),
CurrentColor = config.CappedColor, CurrentColor = config.CappedColor,
DefaultColor = new CurrencySettings().CappedColor, DefaultColor = new CurrencySettings().CappedColor,
@@ -89,8 +91,9 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
IsVisible = true, IsVisible = true,
String = "Color Weekly Limit", String = "Color Max Capacity",
IsChecked = config.ColorWhenLimited, 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 => OnClick = isChecked =>
{ {
config.ColorWhenLimited = isChecked; config.ColorWhenLimited = isChecked;
@@ -103,7 +106,7 @@ public sealed class CurrencyGeneralConfigurationNode : TabbedVerticalListNode
ColorInputRow limitCurrencyColorNode = new ColorInputRow ColorInputRow limitCurrencyColorNode = new ColorInputRow
{ {
Label = "Limit Currency Color", Label = "Max Capacity Color",
Size = new Vector2(300, 24), Size = new Vector2(300, 24),
CurrentColor = config.LimitColor, CurrentColor = config.LimitColor,
DefaultColor = new CurrencySettings().LimitColor, DefaultColor = new CurrencySettings().LimitColor,
@@ -2,11 +2,11 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Currency; namespace AetherBags.Nodes.Configuration.Currency;
public sealed class CurrencyScrollingAreaNode : ScrollingAreaNode<VerticalListNode> public sealed class CurrencyScrollingAreaNode : ScrollingListNode
{ {
public CurrencyScrollingAreaNode() public CurrencyScrollingAreaNode()
{ {
ContentNode.AddNode(new CurrencyGeneralConfigurationNode AddNode(new CurrencyGeneralConfigurationNode
{ {
Size = Size Size = Size
}); });
@@ -2,6 +2,8 @@ using System;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using AetherBags.Nodes.Input;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
@@ -10,12 +12,16 @@ namespace AetherBags.Nodes.Configuration.General;
internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
{ {
private readonly CheckboxNode _hideDefaultBagsCheckboxNode; private readonly CheckboxNode _hideDefaultBagsCheckboxNode;
private readonly CheckboxNode _hideSaddlebagsCheckboxNode;
private readonly CheckboxNode _hideRetainerbagsCheckboxNode;
private readonly LabeledDropdownNode _stackDropDown; private readonly LabeledDropdownNode _stackDropDown;
public FunctionalConfigurationNode() public FunctionalConfigurationNode()
{ {
GeneralSettings config = System.Config.General; GeneralSettings config = System.Config.General;
ItemVerticalSpacing = 2;
var titleNode = new CategoryTextNode var titleNode = new CategoryTextNode
{ {
Height = 18, Height = 18,
@@ -55,6 +61,66 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
AddNode(_hideDefaultBagsCheckboxNode); AddNode(_hideDefaultBagsCheckboxNode);
SubtractTab(1); 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 var linkItemCheckBox = new CheckboxNode
{ {
Size = Size with { Y = 18 }, Size = Size with { Y = 18 },
@@ -68,6 +134,29 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
}; };
AddNode(linkItemCheckBox); 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 _stackDropDown = new LabeledDropdownNode
{ {
Size = new Vector2(300, 20), Size = new Vector2(300, 20),
@@ -81,7 +170,7 @@ internal sealed class FunctionalConfigurationNode : TabbedVerticalListNode
if (Enum.TryParse<InventoryStackMode>(selected, out var parsed)) if (Enum.TryParse<InventoryStackMode>(selected, out var parsed))
{ {
config.StackMode = parsed; config.StackMode = parsed;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
} }
}; };
@@ -5,30 +5,30 @@ using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.General; namespace AetherBags.Nodes.Configuration.General;
public sealed class GeneralScrollingAreaNode : ScrollingAreaNode<VerticalListNode> public sealed class GeneralScrollingAreaNode : ScrollingListNode
{ {
private readonly CheckboxNode _debugCheckboxNode = null!;
public GeneralScrollingAreaNode() public GeneralScrollingAreaNode()
{ {
GeneralSettings config = System.Config.General; 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), Size = new Vector2(300, 20),
IsVisible = true, IsVisible = true,
String = "Debug Mode", String = "Debug Mode",
IsChecked = config.DebugEnabled, IsChecked = config.DebugEnabled,
OnClick = isChecked => { config.DebugEnabled = isChecked; } OnClick = isChecked =>
}; {
ContentNode.AddNode(_debugCheckboxNode); 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.Classes.Timelines;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
using System.Numerics; using System.Numerics;
using AetherBags.Inventory;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
namespace AetherBags.Nodes.Configuration.Layout; namespace AetherBags.Nodes.Configuration.Layout;
@@ -33,7 +34,7 @@ internal sealed class CompactLookaheadNode : SimpleComponentNode
OnValueUpdate = value => OnValueUpdate = value =>
{ {
config.CompactLookahead = value; config.CompactLookahead = value;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
CompactLookahead.AttachNode(this); CompactLookahead.AttachNode(this);
@@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.Inventory;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Nodes.Configuration.Layout; namespace AetherBags.Nodes.Configuration.Layout;
@@ -32,7 +33,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.ShowCategoryItemCount = isChecked; config.ShowCategoryItemCount = isChecked;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(showCategoryItemAmountCheckboxNode); AddNode(showCategoryItemAmountCheckboxNode);
@@ -49,7 +50,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
_preferLargestFitCheckboxNode.IsEnabled = isChecked; _preferLargestFitCheckboxNode.IsEnabled = isChecked;
_useStableInsertCheckboxNode.IsEnabled = isChecked; _useStableInsertCheckboxNode.IsEnabled = isChecked;
_compactLookaheadNode.CompactLookahead.IsEnabled = isChecked; _compactLookaheadNode.CompactLookahead.IsEnabled = isChecked;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(compactPackingCheckboxNode); AddNode(compactPackingCheckboxNode);
@@ -65,7 +66,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.CompactPreferLargestFit = isChecked; config.CompactPreferLargestFit = isChecked;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(_preferLargestFitCheckboxNode); AddNode(_preferLargestFitCheckboxNode);
@@ -80,7 +81,7 @@ internal class LayoutConfigurationNode : TabbedVerticalListNode
OnClick = isChecked => OnClick = isChecked =>
{ {
config.CompactStableInsert = isChecked; config.CompactStableInsert = isChecked;
System.AddonInventoryWindow.ManualInventoryRefresh(); InventoryOrchestrator.RefreshAll(updateMaps: true);
} }
}; };
AddNode(_useStableInsertCheckboxNode); 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 FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
namespace AetherBags.Nodes; namespace AetherBags.Nodes.Input;
public class LabeledDropdownNode : SimpleComponentNode { public class LabeledDropdownNode : SimpleComponentNode {
private readonly GridNode _gridNode; 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;
using System.Numerics; using System.Numerics;
using AetherBags.Helpers; using AetherBags.Helpers;
using AetherBags.Hooks;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Categories;
using AetherBags.Inventory.Items;
using AetherBags.Nodes.Layout; using AetherBags.Nodes.Layout;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
@@ -11,15 +14,12 @@ using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
// TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
namespace AetherBags.Nodes.Inventory; namespace AetherBags.Nodes.Inventory;
public class InventoryCategoryNode : SimpleComponentNode public class InventoryCategoryNode : SimpleComponentNode
{ {
private readonly TextNode _categoryNameTextNode; private readonly TextNode _categoryNameTextNode;
private readonly HybridDirectionalFlexNode<DragDropFixedNode> _itemGridNode; private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
private const float FallbackItemSize = 46; private const float FallbackItemSize = 46;
private const float HeaderHeight = 16; private const float HeaderHeight = 16;
@@ -33,6 +33,8 @@ public class InventoryCategoryNode : SimpleComponentNode
private string _fullHeaderText = string.Empty; private string _fullHeaderText = string.Empty;
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged; public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
public Action? OnRefreshRequested { get; set; }
public Action? OnDragEnd { get; set; }
public InventoryCategoryNode() public InventoryCategoryNode()
{ {
@@ -51,7 +53,7 @@ public class InventoryCategoryNode : SimpleComponentNode
_categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision); _categoryNameTextNode.AddFlags(NodeFlags.EmitsEvents | NodeFlags.HasCollision);
_categoryNameTextNode.AttachNode(this); _categoryNameTextNode.AttachNode(this);
_itemGridNode = new HybridDirectionalFlexNode<DragDropFixedNode> _itemGridNode = new HybridDirectionalFlexNode<DragDropNode>
{ {
Position = new Vector2(0, HeaderHeight), Position = new Vector2(0, HeaderHeight),
Size = new Vector2(240, 92), Size = new Vector2(240, 92),
@@ -218,26 +220,35 @@ public class InventoryCategoryNode : SimpleComponentNode
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data) private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
{ {
InventoryItem item = data.Item; 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 return new InventoryDragDropNode
{ {
Size = new Vector2(42, 46), 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, IsVisible = true,
IconId = item.IconId, IconId = item.IconId,
AcceptedType = DragDropType.Item, AcceptedType = DragDropType.Item,
IsDraggable = !data.IsSlotBlocked, Payload = nodePayload,
Payload = new DragDropPayload
{
Type = DragDropType.Item,
Int1 = location.Container,
Int2 = location.Slot,
},
IsClickable = true, IsClickable = true,
OnDiscard = node => OnDiscard(node, data), OnDiscard = node => OnDiscard(node, data),
OnEnd = _ => System.AddonInventoryWindow.ManualInventoryRefresh(), OnEnd = _ => OnDragEnd?.Invoke(),
OnPayloadAccepted = (node, payload) => OnPayloadAccepted(node, payload, data), OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data),
OnRollOver = node => OnRollOver = node =>
{ {
BeginHeaderHover(); 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) private unsafe void OnDiscard(DragDropNode node, ItemInfo item)
{ {
uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id; uint addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
AgentInventoryContext.Instance()->DiscardItem(item.Item.GetLinkedItem(), item.Item.Container, item.Item.Slot, addonId); 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; try
if (!payload.IsValidInventoryPayload)
{ {
Services.Logger.Warning($"[OnPayload] Invalid payload type: {payload.Type}"); // KTK clears node.Payload before invoking this, so setting it manually again
return; var nodePayload = new DragDropPayload
}
InventoryLocation sourceLocation = payload.InventoryLocation;
if (!sourceLocation.IsValid)
{ {
Services.Logger.Warning($"[OnPayload] Could not resolve source from payload"); Type = DragDropType.Item,
return; Int1 = targetItemInfo.VisualLocation.Container,
} Int2 = targetItemInfo.VisualLocation.Slot,
ReferenceIndex = (short)(targetItemInfo.Item.Container.GetInventoryStartIndex + targetItemInfo.VisualLocation.Slot)
InventoryLocation targetLocation = new InventoryLocation(
item.Container,
(ushort)item.Slot
);
if (sourceLocation.Container.IsSameContainerGroup(targetLocation.Container))
{
Services.Logger.Debug($"[OnPayload] Source and target are in the same container group; no move performed");
node.Payload = payload;
node.IconId = item.IconId;
System.AddonInventoryWindow.ManualInventoryRefresh();
return;
}; };
Services.Logger.Debug($"[OnPayload] Moving {sourceLocation} -> {targetLocation}"); 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( if (!acceptedPayload.IsValidInventoryPayload || !nodePayload.IsValidInventoryPayload)
sourceLocation.Container, sourceLocation.Slot, {
targetLocation.Container, targetLocation.Slot Services.Logger.Warning($"[OnPayload] Invalid payload type: Accepted={acceptedPayload.Type} Node={nodePayload.Type}");
); return;
System.AddonInventoryWindow.ManualInventoryRefresh(); }
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 System.Numerics;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Items;
using Dalamud.Game.ClientState.Keys; using Dalamud.Game.ClientState.Keys;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent; using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Nodes; using KamiToolKit.Nodes;
// TODO: Switch back to CS version when Dalamud Updated
using DragDropFixedNode = AetherBags.Nodes.DragDropNode;
namespace AetherBags.Nodes.Inventory; namespace AetherBags.Nodes.Inventory;
public class InventoryDragDropNode : DragDropFixedNode public class InventoryDragDropNode : DragDropNode
{ {
private readonly TextNode _quantityTextNode; private readonly TextNode _quantityTextNode;
public unsafe InventoryDragDropNode() public unsafe InventoryDragDropNode()
@@ -2,6 +2,7 @@ using System.Collections.Generic;
using System.Numerics; using System.Numerics;
using AetherBags.Currency; using AetherBags.Currency;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.State;
using AetherBags.Nodes.Currency; using AetherBags.Nodes.Currency;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
@@ -24,7 +25,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode
FontType = FontType.MiedingerMed, FontType = FontType.MiedingerMed,
TextFlags = TextFlags.Glare, TextFlags = TextFlags.Glare,
TextColor = ColorHelper.GetColor(50), TextColor = ColorHelper.GetColor(50),
TextOutlineColor = ColorHelper.GetColor(32) // Could also be Color 65 TextOutlineColor = ColorHelper.GetColor(32)
}; };
_slotAmountTextNode.AttachNode(this); _slotAmountTextNode.AttachNode(this);
@@ -46,8 +47,8 @@ public sealed class InventoryFooterNode : SimpleComponentNode
IReadOnlyList<CurrencyInfo> currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]); IReadOnlyList<CurrencyInfo> currencyInfoList = InventoryState.GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>( _currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
dataList: currencyInfoList, dataList: currencyInfoList,
getKeyFromData: c => c.ItemId, getKeyFromData: currencyInfo => currencyInfo.ItemId,
getKeyFromNode: n => n.Currency.ItemId, getKeyFromNode: node => node.Currency.ItemId,
updateNode: (node, data) => updateNode: (node, data) =>
{ {
node.Currency = data; node.Currency = data;
@@ -1,5 +1,6 @@
using System.Numerics; using System.Numerics;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
using KamiToolKit.Classes; using KamiToolKit.Classes;
using KamiToolKit.Classes.Timelines; using KamiToolKit.Classes.Timelines;
@@ -87,7 +88,7 @@ public sealed class InventoryNotificationNode : SimpleComponentNode
Timeline?.PlayAnimation(101); Timeline?.PlayAnimation(101);
} }
} } = null!;
// Future Zeff, this always goes on a parent // Future Zeff, this always goes on a parent
private Timeline ParentLabels => new TimelineBuilder() 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.Helpers;
using AetherBags.Hooks; using AetherBags.Hooks;
using AetherBags.Inventory; using AetherBags.Inventory;
using AetherBags.Inventory.Context;
using AetherBags.Inventory.State;
using AetherBags.IPC;
using Dalamud.Game.Gui;
using Dalamud.Plugin; using Dalamud.Plugin;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using KamiToolKit; using KamiToolKit;
namespace AetherBags; namespace AetherBags;
public unsafe class Plugin : IDalamudPlugin public unsafe class Plugin : IDalamudPlugin
{ {
private static string HelpDescription => "Opens your inventory.";
private readonly CommandHandler _commandHandler; private readonly CommandHandler _commandHandler;
private readonly InventoryHooks _inventoryHooks; private readonly InventoryHooks _inventoryHooks;
private readonly InventoryLifecycles _inventoryLifecycles; private readonly InventoryLifecycles _inventoryLifecycles;
@@ -22,19 +25,35 @@ public unsafe class Plugin : IDalamudPlugin
{ {
pluginInterface.Create<Services>(); pluginInterface.Create<Services>();
System.Config = Util.LoadConfigOrDefault();
BackupHelper.DoConfigBackup(pluginInterface); BackupHelper.DoConfigBackup(pluginInterface);
KamiToolKitLibrary.Initialize(pluginInterface); KamiToolKitLibrary.Initialize(pluginInterface);
System.Config = Util.LoadConfigOrDefault(); System.IPC = new IPCService();
System.AddonInventoryWindow = new AddonInventoryWindow System.AddonInventoryWindow = new AddonInventoryWindow
{ {
InternalName = "AetherBags", InternalName = "AetherBags_MainBags",
Title = "AetherBags", Title = "AetherBags",
Size = new Vector2(750, 750), 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 System.AddonConfigurationWindow = new AddonConfigurationWindow
{ {
InternalName = "AetherBags Config", InternalName = "AetherBags Config",
@@ -47,8 +66,6 @@ public unsafe class Plugin : IDalamudPlugin
_commandHandler = new CommandHandler(); _commandHandler = new CommandHandler();
// Services.GameInventory.InventoryChanged += InventoryState.OnRawItemAdded;
Services.ClientState.Login += OnLogin; Services.ClientState.Login += OnLogin;
Services.ClientState.Logout += OnLogout; Services.ClientState.Logout += OnLogout;
@@ -62,22 +79,20 @@ public unsafe class Plugin : IDalamudPlugin
public void Dispose() public void Dispose()
{ {
Util.SaveConfig(System.Config); InventoryAddonContextMenu.Close();
// Services.GameInventory.InventoryChanged -= InventoryState.OnRawItemAdded;
Services.ClientState.Login -= OnLogin;
Services.ClientState.Logout -= OnLogout;
_commandHandler.Dispose();
System.AddonInventoryWindow.Dispose();
System.AddonConfigurationWindow.Dispose();
KamiToolKitLibrary.Dispose();
_inventoryHooks.Dispose(); _inventoryHooks.Dispose();
_inventoryLifecycles.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() private void OnLogin()
@@ -96,6 +111,8 @@ public unsafe class Plugin : IDalamudPlugin
Util.SaveConfig(System.Config); Util.SaveConfig(System.Config);
InventoryState.TrackLootedItems = false; InventoryState.TrackLootedItems = false;
System.AddonInventoryWindow.Close(); System.AddonInventoryWindow.Close();
System.AddonSaddleBagWindow.Close();
System.AddonRetainerWindow.Close();
System.AddonConfigurationWindow.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 IChatGui ChatGui { get; set; } = null!;
[PluginService] public static IClientState ClientState { get; private set; } = null!; [PluginService] public static IClientState ClientState { get; private set; } = null!;
[PluginService] public static ICommandManager CommandManager { 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 IDataManager DataManager { get; set; } = null!;
[PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!; [PluginService] public static IDalamudPluginInterface PluginInterface { get; private set; } = null!;
[PluginService] public static IFramework Framework { 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 IGameGui GameGui { get; private set; } = null!;
[PluginService] public static IGameInventory GameInventory { get; set; } = null!; [PluginService] public static IGameInventory GameInventory { get; set; } = null!;
[PluginService] public static IKeyState KeyState { get; private 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 IPluginLog Logger { get; private set; } = null!;
[PluginService] public static INotificationManager NotificationManager { 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 ISigScanner SigScanner { get; private set; } = null!;
[PluginService] public static IGameInteropProvider GameInteropProvider { 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.Addons;
using AetherBags.Configuration; using AetherBags.Configuration;
using AetherBags.IPC;
namespace AetherBags; namespace AetherBags;
public static class System public static class System
{ {
public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!; public static AddonInventoryWindow AddonInventoryWindow { get; set; } = null!;
public static AddonSaddleBagWindow AddonSaddleBagWindow { get; set; } = null!;
public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!;
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!; public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
public static IPCService IPC { get; set; } = null!;
public static SystemConfiguration Config { get; set; } = null!; public static SystemConfiguration Config { get; set; } = null!;
} }