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