Merge branch 'dev/pie-lover'
This commit is contained in:
@@ -25,16 +25,20 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
{
|
{
|
||||||
InitializeBackgroundDropTarget();
|
InitializeBackgroundDropTarget();
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
HorizontalSpacing = CategorySpacing,
|
ContentHeight = 0f,
|
||||||
VerticalSpacing = CategorySpacing,
|
AutoHideScrollBar = true,
|
||||||
TopPadding = 4.0f,
|
|
||||||
BottomPadding = 4.0f,
|
|
||||||
};
|
};
|
||||||
CategoriesNode.AttachNode(this);
|
ScrollableCategories.AttachNode(this);
|
||||||
|
|
||||||
|
CategoriesNode = ScrollableCategories.ContentNode;
|
||||||
|
CategoriesNode.HorizontalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.VerticalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.TopPadding = 4.0f;
|
||||||
|
CategoriesNode.BottomPadding = 4.0f;
|
||||||
|
|
||||||
_lootedCategoryNode = new LootedItemsCategoryNode
|
_lootedCategoryNode = new LootedItemsCategoryNode
|
||||||
{
|
{
|
||||||
@@ -127,6 +131,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
||||||
}
|
}
|
||||||
|
CategoriesNode.InvalidateLayout();
|
||||||
AutoSizeWindow();
|
AutoSizeWindow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,6 +139,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
private void OnDismissLootedItem(int index)
|
private void OnDismissLootedItem(int index)
|
||||||
{
|
{
|
||||||
System.LootedItemsTracker.RemoveByIndex(index);
|
System.LootedItemsTracker.RemoveByIndex(index);
|
||||||
|
System.LootedItemsTracker.FlushPendingChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnClearAllLootedItems()
|
private void OnClearAllLootedItems()
|
||||||
@@ -148,6 +154,21 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
FooterNode.RefreshCurrencies();
|
FooterNode.RefreshCurrencies();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void UpdateHeaderLayout()
|
||||||
|
{
|
||||||
|
base.UpdateHeaderLayout();
|
||||||
|
|
||||||
|
AtkUnitBase* addon = this;
|
||||||
|
if (addon == null) return;
|
||||||
|
|
||||||
|
var header = CalculateHeaderLayout(addon);
|
||||||
|
|
||||||
|
if (_notificationNode != null)
|
||||||
|
{
|
||||||
|
_notificationNode.Size = new Vector2(header.HeaderWidth, 28f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void SetNotification(InventoryNotificationInfo info)
|
public void SetNotification(InventoryNotificationInfo info)
|
||||||
{
|
{
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
@@ -168,6 +189,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
|||||||
|
|
||||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||||
|
|
||||||
|
_lootedCategoryNode?.Dispose();
|
||||||
|
|
||||||
IsSetupComplete = false;
|
IsSetupComplete = false;
|
||||||
base.OnFinalize(addon);
|
base.OnFinalize(addon);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
|
|
||||||
private readonly Vector3 _tintColor = new(8f / 255f, -8f / 255f, -4f / 255f);
|
private readonly Vector3 _tintColor = new(8f / 255f, -8f / 255f, -4f / 255f);
|
||||||
|
|
||||||
protected override float MinWindowWidth => 400;
|
protected override float MinWindowWidth => 500;
|
||||||
protected override float MaxWindowWidth => 700;
|
protected override float MaxWindowWidth => 700;
|
||||||
|
|
||||||
private readonly string[] _retainerAddonNames = { "InventoryRetainer", "InventoryRetainerLarge" };
|
private readonly string[] _retainerAddonNames = { "InventoryRetainer", "InventoryRetainerLarge" };
|
||||||
@@ -38,16 +38,20 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
|
|
||||||
WindowNode?.AddColor = _tintColor;
|
WindowNode?.AddColor = _tintColor;
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
HorizontalSpacing = CategorySpacing,
|
ContentHeight = 0f,
|
||||||
VerticalSpacing = CategorySpacing,
|
AutoHideScrollBar = true,
|
||||||
TopPadding = 4.0f,
|
|
||||||
BottomPadding = 4.0f,
|
|
||||||
};
|
};
|
||||||
CategoriesNode.AttachNode(this);
|
ScrollableCategories.AttachNode(this);
|
||||||
|
|
||||||
|
CategoriesNode = ScrollableCategories.ContentNode;
|
||||||
|
CategoriesNode.HorizontalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.VerticalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.TopPadding = 4.0f;
|
||||||
|
CategoriesNode.BottomPadding = 4.0f;
|
||||||
|
|
||||||
var header = CalculateHeaderLayout(addon);
|
var header = CalculateHeaderLayout(addon);
|
||||||
|
|
||||||
@@ -90,7 +94,6 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
|||||||
};
|
};
|
||||||
_entrustDuplicatesButton.AttachNode(this);
|
_entrustDuplicatesButton.AttachNode(this);
|
||||||
|
|
||||||
// Slot counter
|
|
||||||
_slotCounterNode = new TextNode
|
_slotCounterNode = new TextNode
|
||||||
{
|
{
|
||||||
Position = new Vector2(Size.X - 10, 0),
|
Position = new Vector2(Size.X - 10, 0),
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
private readonly Vector3 _tintColor = new (-16f / 255f, -4f / 255f, 8f / 255f);
|
private readonly Vector3 _tintColor = new (-16f / 255f, -4f / 255f, 8f / 255f);
|
||||||
|
|
||||||
protected override float MinWindowWidth => 400;
|
protected override float MinWindowWidth => 500;
|
||||||
protected override float MaxWindowWidth => 600;
|
protected override float MaxWindowWidth => 600;
|
||||||
|
|
||||||
protected override void OnSetup(AtkUnitBase* addon)
|
protected override void OnSetup(AtkUnitBase* addon)
|
||||||
@@ -31,16 +31,20 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
|||||||
|
|
||||||
WindowNode?.AddColor = _tintColor;
|
WindowNode?.AddColor = _tintColor;
|
||||||
|
|
||||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||||
{
|
{
|
||||||
Position = ContentStartPosition,
|
Position = ContentStartPosition,
|
||||||
Size = ContentSize,
|
Size = ContentSize,
|
||||||
HorizontalSpacing = CategorySpacing,
|
ContentHeight = 0f,
|
||||||
VerticalSpacing = CategorySpacing,
|
AutoHideScrollBar = true,
|
||||||
TopPadding = 4.0f,
|
|
||||||
BottomPadding = 4.0f,
|
|
||||||
};
|
};
|
||||||
CategoriesNode.AttachNode(this);
|
ScrollableCategories.AttachNode(this);
|
||||||
|
|
||||||
|
CategoriesNode = ScrollableCategories.ContentNode;
|
||||||
|
CategoriesNode.HorizontalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.VerticalSpacing = CategorySpacing;
|
||||||
|
CategoriesNode.TopPadding = 4.0f;
|
||||||
|
CategoriesNode.BottomPadding = 4.0f;
|
||||||
|
|
||||||
var header = CalculateHeaderLayout(addon);
|
var header = CalculateHeaderLayout(addon);
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
||||||
|
|
||||||
protected DragDropNode BackgroundDropTarget = null!;
|
protected DragDropNode BackgroundDropTarget = null!;
|
||||||
|
protected ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!;
|
||||||
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
||||||
protected TextInputWithButtonNode SearchInputNode = null!;
|
protected TextInputWithButtonNode SearchInputNode = null!;
|
||||||
protected InventoryFooterNode FooterNode = null!;
|
protected InventoryFooterNode FooterNode = null!;
|
||||||
@@ -37,6 +38,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
internal ContextMenu ContextMenu = null!;
|
internal ContextMenu ContextMenu = null!;
|
||||||
|
|
||||||
|
protected readonly SharedNodePool<InventoryDragDropNode> SharedItemNodePool = new(
|
||||||
|
maxSize: 256,
|
||||||
|
factory: null,
|
||||||
|
resetAction: node => node.ResetForReuse());
|
||||||
|
|
||||||
|
protected readonly SharedNodePool<InventoryCategoryNode> SharedCategoryNodePool = new(
|
||||||
|
maxSize: 32,
|
||||||
|
factory: null,
|
||||||
|
resetAction: node => node.ResetForReuse());
|
||||||
|
|
||||||
|
protected readonly VirtualizationState CategoryVirtualization = new() { BufferSize = 200f };
|
||||||
|
|
||||||
protected virtual float MinWindowWidth => 600;
|
protected virtual float MinWindowWidth => 600;
|
||||||
protected virtual float MaxWindowWidth => 800;
|
protected virtual float MaxWindowWidth => 800;
|
||||||
protected virtual float MinWindowHeight => 200;
|
protected virtual float MinWindowHeight => 200;
|
||||||
@@ -47,11 +60,16 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
protected const float ItemPadding = 5;
|
protected const float ItemPadding = 5;
|
||||||
protected const float FooterHeight = 28f;
|
protected const float FooterHeight = 28f;
|
||||||
protected const float FooterTopSpacing = 4f;
|
protected const float FooterTopSpacing = 4f;
|
||||||
protected const float SettingsButtonOffset = 48f;
|
protected const float SettingsButtonOffset = 62f;
|
||||||
|
protected const float ScrollBarWidth = 16f;
|
||||||
|
protected const float ContentHeightOffset = 4f;
|
||||||
|
|
||||||
protected bool RefreshQueued;
|
protected bool RefreshQueued;
|
||||||
protected bool RefreshAutosizeQueued;
|
protected bool RefreshAutosizeQueued;
|
||||||
protected bool IsSetupComplete;
|
protected bool IsSetupComplete;
|
||||||
|
private bool _deferredPopulationInProgress;
|
||||||
|
private bool _initialPopulationComplete;
|
||||||
|
private const int ItemsPerFrame = 50;
|
||||||
|
|
||||||
protected abstract InventoryStateBase InventoryState { get; }
|
protected abstract InventoryStateBase InventoryState { get; }
|
||||||
|
|
||||||
@@ -61,6 +79,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
private readonly HashSet<uint> _searchMatchScratch = new();
|
private readonly HashSet<uint> _searchMatchScratch = new();
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
|
private string _lastSearchText = string.Empty;
|
||||||
|
|
||||||
private int _requestedUpdateCount;
|
private int _requestedUpdateCount;
|
||||||
private int _refreshFromLifecycleCount;
|
private int _refreshFromLifecycleCount;
|
||||||
@@ -72,6 +91,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
public InventoryStats GetStats() => InventoryState.GetStats();
|
public InventoryStats GetStats() => InventoryState.GetStats();
|
||||||
|
|
||||||
|
public IReadOnlyList<CategorizedInventory>? GetVisibleCategories()
|
||||||
|
{
|
||||||
|
if (!IsSetupComplete) return null;
|
||||||
|
string filter = GetSearchText();
|
||||||
|
return InventoryState.GetCategories(filter);
|
||||||
|
}
|
||||||
|
|
||||||
public virtual void SetSearchText(string searchText)
|
public virtual void SetSearchText(string searchText)
|
||||||
{
|
{
|
||||||
Services.Framework.RunOnTick(() =>
|
Services.Framework.RunOnTick(() =>
|
||||||
@@ -109,6 +135,12 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
string searchText = SearchInputNode.SearchString.ExtractText();
|
string searchText = SearchInputNode.SearchString.ExtractText();
|
||||||
bool isSearching = !string.IsNullOrWhiteSpace(searchText);
|
bool isSearching = !string.IsNullOrWhiteSpace(searchText);
|
||||||
|
|
||||||
|
if (searchText != _lastSearchText)
|
||||||
|
{
|
||||||
|
_lastSearchText = searchText;
|
||||||
|
System.AetherBagsAPI?.API.RaiseSearchChanged(searchText);
|
||||||
|
}
|
||||||
|
|
||||||
if (config.SearchMode == SearchMode.Highlight && isSearching)
|
if (config.SearchMode == SearchMode.Highlight && isSearching)
|
||||||
{
|
{
|
||||||
_searchMatchScratch.Clear();
|
_searchMatchScratch.Clear();
|
||||||
@@ -154,17 +186,20 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X;
|
float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X;
|
||||||
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
|
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
|
||||||
|
|
||||||
|
bool deferItems = !_deferredPopulationInProgress && !_initialPopulationComplete;
|
||||||
|
|
||||||
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
||||||
dataList: categories,
|
dataList: categories,
|
||||||
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
||||||
getKeyFromNode: node => node.CategorizedInventory.Key,
|
getKeyFromNode: node => node.CategorizedInventory.Key,
|
||||||
updateNode: (node, data) =>
|
updateNode: (node, data) =>
|
||||||
{
|
{
|
||||||
node.MaxWidth = maxContentWidth;
|
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine), deferItemCreation: deferItems);
|
||||||
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine));
|
if (!deferItems) node.RefreshNodeVisuals();
|
||||||
node.RefreshNodeVisuals();
|
|
||||||
},
|
},
|
||||||
createNodeMethod: _ => CreateCategoryNode(maxContentWidth));
|
createNodeMethod: _ => CreateCategoryNode(),
|
||||||
|
resetNodeForReuse: ResetCategoryNodeForReuse,
|
||||||
|
externalPool: SharedCategoryNodePool);
|
||||||
|
|
||||||
if (HasPinning)
|
if (HasPinning)
|
||||||
{
|
{
|
||||||
@@ -174,6 +209,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
WireHoverHandlers();
|
WireHoverHandlers();
|
||||||
|
|
||||||
|
CategoriesNode.InvalidateLayout();
|
||||||
|
|
||||||
if (autosize)
|
if (autosize)
|
||||||
AutoSizeWindow();
|
AutoSizeWindow();
|
||||||
else
|
else
|
||||||
@@ -181,6 +218,104 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
LayoutContent();
|
LayoutContent();
|
||||||
CategoriesNode.RecalculateLayout();
|
CategoriesNode.RecalculateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (deferItems && !_deferredPopulationInProgress)
|
||||||
|
{
|
||||||
|
StartDeferredItemPopulation();
|
||||||
|
}
|
||||||
|
else if (!deferItems && !_initialPopulationComplete)
|
||||||
|
{
|
||||||
|
_initialPopulationComplete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
System.AetherBagsAPI?.API.RaiseCategoriesRefreshed();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartDeferredItemPopulation()
|
||||||
|
{
|
||||||
|
_deferredPopulationInProgress = true;
|
||||||
|
Services.Framework.RunOnTick(PopulateCategoryBatch, delayTicks: 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopulateCategoryBatch()
|
||||||
|
{
|
||||||
|
if (!IsOpen)
|
||||||
|
{
|
||||||
|
_deferredPopulationInProgress = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCategoryVisibility();
|
||||||
|
|
||||||
|
int itemsPopulated = 0;
|
||||||
|
using (CategoriesNode.DeferRecalculateLayout())
|
||||||
|
{
|
||||||
|
var nodes = CategoriesNode.Nodes;
|
||||||
|
for (int i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (nodes[i] is not InventoryCategoryNode categoryNode || !categoryNode.NeedsItemPopulation)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!CategoryVirtualization.IsVisible(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int categoryItemCount = categoryNode.CategorizedInventory.Items.Count;
|
||||||
|
|
||||||
|
if (itemsPopulated > 0 && itemsPopulated + categoryItemCount > ItemsPerFrame)
|
||||||
|
break;
|
||||||
|
|
||||||
|
categoryNode.PopulateItems();
|
||||||
|
categoryNode.RefreshNodeVisuals();
|
||||||
|
itemsPopulated += categoryItemCount;
|
||||||
|
|
||||||
|
if (itemsPopulated >= ItemsPerFrame)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (itemsPopulated < ItemsPerFrame)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (nodes[i] is not InventoryCategoryNode categoryNode || !categoryNode.NeedsItemPopulation)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (CategoryVirtualization.IsVisible(i))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
int categoryItemCount = categoryNode.CategorizedInventory.Items.Count;
|
||||||
|
|
||||||
|
if (itemsPopulated > 0 && itemsPopulated + categoryItemCount > ItemsPerFrame)
|
||||||
|
break;
|
||||||
|
|
||||||
|
categoryNode.PopulateItems();
|
||||||
|
categoryNode.RefreshNodeVisuals();
|
||||||
|
itemsPopulated += categoryItemCount;
|
||||||
|
|
||||||
|
if (itemsPopulated >= ItemsPerFrame)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasMore = false;
|
||||||
|
foreach (var node in CategoriesNode.Nodes)
|
||||||
|
{
|
||||||
|
if (node is InventoryCategoryNode categoryNode && categoryNode.NeedsItemPopulation)
|
||||||
|
{
|
||||||
|
hasMore = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMore)
|
||||||
|
{
|
||||||
|
Services.Framework.RunOnTick(PopulateCategoryBatch);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_deferredPopulationInProgress = false;
|
||||||
|
_initialPopulationComplete = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected readonly struct HeaderLayout
|
protected readonly struct HeaderLayout
|
||||||
@@ -196,11 +331,38 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
var header = addon->WindowHeaderCollisionNode;
|
var header = addon->WindowHeaderCollisionNode;
|
||||||
float headerW = header->Width;
|
float headerW = header->Width;
|
||||||
|
|
||||||
float settingsX = headerW - 62f;
|
|
||||||
float itemY = header->Y + (header->Height - 28f) * 0.5f;
|
float itemY = header->Y + (header->Height - 28f) * 0.5f;
|
||||||
|
|
||||||
float searchWidth = headerW * 0.45f;
|
// Reserve space for close button (~50px) and settings button (~48px + gap)
|
||||||
float searchX = (headerW - searchWidth) * 0.5f;
|
const float closeButtonReserve = 50f;
|
||||||
|
const float settingsButtonWidth = 28f;
|
||||||
|
const float minGap = 16f;
|
||||||
|
const float minSearchWidth = 150f;
|
||||||
|
const float maxSearchWidth = 350f;
|
||||||
|
|
||||||
|
// Calculate max available width for search bar
|
||||||
|
// Layout from right: [closeButton 50px] [settings 28px] [gap 16px] [searchBar] [gap 16px] [leftContent]
|
||||||
|
float rightReserve = closeButtonReserve + settingsButtonWidth + minGap;
|
||||||
|
float leftReserve = 220f; // Space for title (e.g. "Chocobo Saddlebag" is ~200px)
|
||||||
|
float availableForSearch = headerW - rightReserve - leftReserve;
|
||||||
|
|
||||||
|
// Search bar width: prefer 45% of header, but clamp to available space and min/max
|
||||||
|
float desiredSearchWidth = headerW * 0.45f;
|
||||||
|
float searchWidth = Math.Clamp(desiredSearchWidth, minSearchWidth, Math.Min(maxSearchWidth, availableForSearch));
|
||||||
|
|
||||||
|
// Center the search bar, but ensure it doesn't extend past the safe right boundary
|
||||||
|
float maxSearchRight = headerW - rightReserve;
|
||||||
|
float centeredSearchX = (headerW - searchWidth) * 0.5f;
|
||||||
|
float searchRight = centeredSearchX + searchWidth;
|
||||||
|
|
||||||
|
// If centered position would overlap with right elements, shift left
|
||||||
|
float searchX = searchRight > maxSearchRight
|
||||||
|
? maxSearchRight - searchWidth
|
||||||
|
: centeredSearchX;
|
||||||
|
|
||||||
|
// Ensure search bar doesn't go past left reserve
|
||||||
|
if (searchX < leftReserve)
|
||||||
|
searchX = leftReserve;
|
||||||
|
|
||||||
return new HeaderLayout
|
return new HeaderLayout
|
||||||
{
|
{
|
||||||
@@ -231,17 +393,29 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
BackgroundDropTarget.AttachNode(this);
|
BackgroundDropTarget.AttachNode(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual InventoryCategoryNode CreateCategoryNode(float? maxWidth = null)
|
protected virtual InventoryCategoryNode CreateCategoryNode()
|
||||||
{
|
{
|
||||||
return new InventoryCategoryNode
|
var node = SharedCategoryNodePool.TryRent();
|
||||||
|
if (node == null)
|
||||||
|
{
|
||||||
|
node = new InventoryCategoryNode
|
||||||
{
|
{
|
||||||
Size = ContentSize with { Y = 120 },
|
Size = ContentSize with { Y = 120 },
|
||||||
MaxWidth = maxWidth,
|
SharedItemPool = SharedItemNodePool,
|
||||||
OnRefreshRequested = ManualRefresh,
|
|
||||||
OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
node.OnRefreshRequested = ManualRefresh;
|
||||||
|
node.OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true);
|
||||||
|
node.SharedItemPool = SharedItemNodePool;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ResetCategoryNodeForReuse(InventoryCategoryNode node)
|
||||||
|
{
|
||||||
|
node.ResetForReuse();
|
||||||
|
}
|
||||||
|
|
||||||
private void OnBackgroundPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload)
|
private void OnBackgroundPayloadAccepted(DragDropNode node, DragDropPayload acceptedPayload)
|
||||||
{
|
{
|
||||||
if (!acceptedPayload.IsValidInventoryPayload) return;
|
if (!acceptedPayload.IsValidInventoryPayload) return;
|
||||||
@@ -316,17 +490,20 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0);
|
float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0);
|
||||||
if (gridH < 0) gridH = 0;
|
if (gridH < 0) gridH = 0;
|
||||||
|
|
||||||
CategoriesNode.Position = contentPos;
|
ScrollableCategories.Position = contentPos;
|
||||||
CategoriesNode.Size = new Vector2(contentSize.X, gridH);
|
ScrollableCategories.Size = new Vector2(contentSize.X, gridH);
|
||||||
|
|
||||||
UpdateCategoryMaxWidths(contentSize.X);
|
float categoriesWidth = contentSize.X - ScrollBarWidth;
|
||||||
|
CategoriesNode.Width = categoriesWidth;
|
||||||
|
|
||||||
|
UpdateCategoryMaxWidths(categoriesWidth);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCategoryMaxWidths(float maxWidth)
|
private void UpdateCategoryMaxWidths(float maxWidth)
|
||||||
{
|
{
|
||||||
foreach (var node in CategoriesNode.Nodes)
|
foreach (var node in CategoriesNode.Nodes)
|
||||||
{
|
{
|
||||||
if (node is InventoryCategoryNode categoryNode && categoryNode.MaxWidth != maxWidth)
|
if (node is InventoryCategoryNodeBase categoryNode && categoryNode.MaxWidth != maxWidth)
|
||||||
{
|
{
|
||||||
categoryNode.MaxWidth = maxWidth;
|
categoryNode.MaxWidth = maxWidth;
|
||||||
categoryNode.RecalculateSize();
|
categoryNode.RecalculateSize();
|
||||||
@@ -343,7 +520,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
for (int i = 0; i < nodes.Count; i++)
|
for (int i = 0; i < nodes.Count; i++)
|
||||||
{
|
{
|
||||||
if (nodes[i] is not InventoryCategoryNode cat)
|
if (nodes[i] is not InventoryCategoryNodeBase cat)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
childCount++;
|
childCount++;
|
||||||
@@ -354,36 +531,68 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
if (childCount == 0)
|
if (childCount == 0)
|
||||||
{
|
{
|
||||||
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
|
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
|
||||||
|
UpdateScrollParameters();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
float requiredWidth = maxChildWidth + (ContentStartPosition.X * 2);
|
float footerSpace = HasFooter || HasSlotCounter ? FooterHeight + FooterTopSpacing : 0;
|
||||||
|
|
||||||
|
float requiredWidth = maxChildWidth + ScrollBarWidth + (ContentStartPosition.X * 2);
|
||||||
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
|
float finalWidth = Math.Clamp(requiredWidth, MinWindowWidth, MaxWindowWidth);
|
||||||
|
|
||||||
if (SettingsButtonNode != null)
|
if (SettingsButtonNode != null)
|
||||||
{
|
{
|
||||||
SettingsButtonNode.X = finalWidth - 62f;
|
SettingsButtonNode.X = finalWidth - SettingsButtonOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
|
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
|
||||||
|
float categoriesWidth = contentWidth - ScrollBarWidth;
|
||||||
|
|
||||||
float footerSpace = HasFooter || HasSlotCounter ? FooterHeight + FooterTopSpacing : 0;
|
CategoriesNode.Width = categoriesWidth;
|
||||||
float gridBudget = Math.Max(0f, MaxWindowHeight - footerSpace);
|
UpdateCategoryMaxWidths(categoriesWidth);
|
||||||
|
|
||||||
CategoriesNode.Position = ContentStartPosition;
|
|
||||||
CategoriesNode.Size = new Vector2(contentWidth, gridBudget);
|
|
||||||
|
|
||||||
UpdateCategoryMaxWidths(contentWidth);
|
|
||||||
|
|
||||||
CategoriesNode.RecalculateLayout();
|
CategoriesNode.RecalculateLayout();
|
||||||
|
|
||||||
float requiredGridHeight = CategoriesNode.GetRequiredHeight();
|
float requiredGridHeight = CategoriesNode.GetRequiredHeight();
|
||||||
float requiredContentHeight = requiredGridHeight + footerSpace;
|
|
||||||
|
|
||||||
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X;
|
float requiredContentHeight = requiredGridHeight + footerSpace;
|
||||||
|
float requiredWindowHeight = requiredContentHeight + ContentStartPosition.Y + ContentStartPosition.X + ContentHeightOffset;
|
||||||
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
|
float finalHeight = Math.Clamp(requiredWindowHeight, MinWindowHeight, MaxWindowHeight);
|
||||||
|
|
||||||
ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
|
ResizeWindow(finalWidth, finalHeight, recalcLayout: false);
|
||||||
|
|
||||||
|
UpdateScrollParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void UpdateScrollParameters()
|
||||||
|
{
|
||||||
|
if (ScrollableCategories == null) return;
|
||||||
|
|
||||||
|
float requiredHeight = CategoriesNode.GetRequiredHeight();
|
||||||
|
ScrollableCategories.ContentHeight = requiredHeight;
|
||||||
|
|
||||||
|
CategoryVirtualization.ViewportHeight = ScrollableCategories.Size.Y;
|
||||||
|
UpdateCategoryVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnScrollValueChanged(int scrollPosition)
|
||||||
|
{
|
||||||
|
CategoryVirtualization.ScrollPosition = scrollPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateCategoryVisibility()
|
||||||
|
{
|
||||||
|
var nodes = CategoriesNode.Nodes;
|
||||||
|
CategoryVirtualization.SetItemCount(nodes.Count);
|
||||||
|
|
||||||
|
for (int i = 0; i < nodes.Count; i++)
|
||||||
|
{
|
||||||
|
if (nodes[i] is InventoryCategoryNodeBase cat)
|
||||||
|
{
|
||||||
|
CategoryVirtualization.SetItemLayout(i, cat.Y, cat.Height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CategoryVirtualization.UpdateVisibility();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ResizeWindow(float width, float height, bool recalcLayout)
|
protected void ResizeWindow(float width, float height, bool recalcLayout)
|
||||||
@@ -395,10 +604,32 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
BackgroundDropTarget.Size = ContentSize;
|
BackgroundDropTarget.Size = ContentSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateHeaderLayout();
|
||||||
LayoutContent();
|
LayoutContent();
|
||||||
|
|
||||||
if (recalcLayout)
|
if (recalcLayout)
|
||||||
CategoriesNode.RecalculateLayout();
|
CategoriesNode.RecalculateLayout();
|
||||||
|
|
||||||
|
UpdateScrollParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected virtual void UpdateHeaderLayout()
|
||||||
|
{
|
||||||
|
AtkUnitBase* addon = this;
|
||||||
|
if (addon == null) return;
|
||||||
|
|
||||||
|
var header = CalculateHeaderLayout(addon);
|
||||||
|
|
||||||
|
if (SearchInputNode != null)
|
||||||
|
{
|
||||||
|
SearchInputNode.Position = header.SearchPosition;
|
||||||
|
SearchInputNode.Size = header.SearchSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SettingsButtonNode != null)
|
||||||
|
{
|
||||||
|
SettingsButtonNode.Position = new Vector2(header.HeaderWidth - SettingsButtonOffset, header.HeaderY);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ResizeWindow(float width, float height)
|
protected void ResizeWindow(float width, float height)
|
||||||
@@ -438,6 +669,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
{
|
{
|
||||||
ContextMenu = new ContextMenu();
|
ContextMenu = new ContextMenu();
|
||||||
|
|
||||||
|
System.AetherBagsAPI?.API.RaiseInventoryOpened();
|
||||||
|
|
||||||
|
if (ScrollableCategories != null)
|
||||||
|
{
|
||||||
|
ScrollableCategories.ScrollBarNode.OnValueChanged = OnScrollValueChanged;
|
||||||
|
}
|
||||||
|
|
||||||
base.OnSetup(addon);
|
base.OnSetup(addon);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,10 +695,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
|||||||
|
|
||||||
protected override void OnFinalize(AtkUnitBase* addon)
|
protected override void OnFinalize(AtkUnitBase* addon)
|
||||||
{
|
{
|
||||||
|
System.AetherBagsAPI?.API.RaiseInventoryClosed();
|
||||||
|
|
||||||
ContextMenu?.Dispose();
|
ContextMenu?.Dispose();
|
||||||
HoverSubscribed.Clear();
|
HoverSubscribed.Clear();
|
||||||
RefreshQueued = false;
|
RefreshQueued = false;
|
||||||
RefreshAutosizeQueued = false;
|
RefreshAutosizeQueued = false;
|
||||||
|
_deferredPopulationInProgress = false;
|
||||||
|
_initialPopulationComplete = false;
|
||||||
|
|
||||||
|
SharedItemNodePool.Clear();
|
||||||
|
SharedCategoryNodePool.Clear();
|
||||||
|
CategoryVirtualization.ClearLayout();
|
||||||
|
|
||||||
base.OnFinalize(addon);
|
base.OnFinalize(addon);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
|
using KamiToolKit.ContextMenu;
|
||||||
|
|
||||||
|
namespace AetherBags.Addons;
|
||||||
|
|
||||||
|
public static class ItemContextMenuHandler
|
||||||
|
{
|
||||||
|
private static ContextMenu? _itemMenu;
|
||||||
|
|
||||||
|
public static void Initialize()
|
||||||
|
{
|
||||||
|
_itemMenu = new ContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Dispose()
|
||||||
|
{
|
||||||
|
_itemMenu?.Dispose();
|
||||||
|
_itemMenu = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryShowExternalMenu(ItemInfo item)
|
||||||
|
{
|
||||||
|
if (_itemMenu == null) return false;
|
||||||
|
if (!System.Config.General.UseUnifiedExternalCategories) return false;
|
||||||
|
|
||||||
|
var entries = ExternalCategoryManager.GetContextMenuEntries(item.Item.ItemId);
|
||||||
|
if (entries == null || entries.Count == 0) return false;
|
||||||
|
|
||||||
|
_itemMenu.Clear();
|
||||||
|
|
||||||
|
var context = new ContextMenuContext(
|
||||||
|
item.Item.ItemId,
|
||||||
|
(int)item.Item.Container,
|
||||||
|
item.Item.Slot
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
var capturedEntry = entry;
|
||||||
|
var capturedContext = context;
|
||||||
|
_itemMenu.AddItem(entry.Label, () => capturedEntry.OnClick(capturedContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
_itemMenu.Open();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ public class GeneralSettings
|
|||||||
public bool HideGameRetainer { get; set; } = false;
|
public bool HideGameRetainer { get; set; } = false;
|
||||||
public bool ShowCategoryItemCount { get; set; } = false;
|
public bool ShowCategoryItemCount { get; set; } = false;
|
||||||
public bool LinkItemEnabled { get; set; } = false;
|
public bool LinkItemEnabled { get; set; } = false;
|
||||||
|
public bool UseUnifiedExternalCategories { get; set; } = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum InventoryStackMode : byte
|
public enum InventoryStackMode : byte
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Game;
|
|||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
namespace AetherBags.Currency;
|
namespace AetherBags.Currency;
|
||||||
|
|
||||||
@@ -16,6 +16,7 @@ public static unsafe class CurrencyState
|
|||||||
|
|
||||||
private static readonly Dictionary<uint, CurrencyItem> CurrencyItemByCurrencyIdCache = new(capacity: 32);
|
private static readonly Dictionary<uint, CurrencyItem> CurrencyItemByCurrencyIdCache = new(capacity: 32);
|
||||||
private static readonly Dictionary<uint, CurrencyStaticInfo> CurrencyStaticByItemIdCache = new(capacity: 64);
|
private static readonly Dictionary<uint, CurrencyStaticInfo> CurrencyStaticByItemIdCache = new(capacity: 64);
|
||||||
|
private static readonly List<CurrencyInfo> CurrencyInfoScratch = new(capacity: 8);
|
||||||
|
|
||||||
private static uint? _cachedLimitedTomestoneItemId;
|
private static uint? _cachedLimitedTomestoneItemId;
|
||||||
private static uint? _cachedNonLimitedTomestoneItemId;
|
private static uint? _cachedNonLimitedTomestoneItemId;
|
||||||
@@ -29,6 +30,12 @@ public static unsafe class CurrencyState
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
||||||
|
=> GetCurrencyInfoListCore(currencyIds.AsSpan());
|
||||||
|
|
||||||
|
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(List<uint> currencyIds)
|
||||||
|
=> GetCurrencyInfoListCore(CollectionsMarshal.AsSpan(currencyIds));
|
||||||
|
|
||||||
|
private static IReadOnlyList<CurrencyInfo> GetCurrencyInfoListCore(ReadOnlySpan<uint> currencyIds)
|
||||||
{
|
{
|
||||||
if (currencyIds.Length == 0)
|
if (currencyIds.Length == 0)
|
||||||
return Array.Empty<CurrencyInfo>();
|
return Array.Empty<CurrencyInfo>();
|
||||||
@@ -37,7 +44,7 @@ public static unsafe class CurrencyState
|
|||||||
if (inventoryManager == null)
|
if (inventoryManager == null)
|
||||||
return Array.Empty<CurrencyInfo>();
|
return Array.Empty<CurrencyInfo>();
|
||||||
|
|
||||||
List<CurrencyInfo> currencyInfoList = new List<CurrencyInfo>(currencyIds.Length);
|
CurrencyInfoScratch.Clear();
|
||||||
|
|
||||||
for (int i = 0; i < currencyIds.Length; i++)
|
for (int i = 0; i < currencyIds.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -57,7 +64,7 @@ public static unsafe class CurrencyState
|
|||||||
isCapped = weeklyAcquired >= weeklyLimit;
|
isCapped = weeklyAcquired >= weeklyLimit;
|
||||||
}
|
}
|
||||||
|
|
||||||
currencyInfoList.Add(new CurrencyInfo
|
CurrencyInfoScratch.Add(new CurrencyInfo
|
||||||
{
|
{
|
||||||
Amount = amount,
|
Amount = amount,
|
||||||
MaxAmount = staticInfo.MaxAmount,
|
MaxAmount = staticInfo.MaxAmount,
|
||||||
@@ -68,7 +75,7 @@ public static unsafe class CurrencyState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return currencyInfoList;
|
return CurrencyInfoScratch;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static (uint Limited, uint NonLimited) GetCurrentTomestoneIds()
|
public static (uint Limited, uint NonLimited) GetCurrentTomestoneIds()
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
|
|
||||||
|
namespace AetherBags.IPC.AetherBagsAPI;
|
||||||
|
|
||||||
|
public class AetherBagsAPIImpl : IAetherBagsAPI
|
||||||
|
{
|
||||||
|
public event Action<uint>? OnItemHovered;
|
||||||
|
public event Action<uint>? OnItemUnhovered;
|
||||||
|
public event Action<uint>? OnItemClicked;
|
||||||
|
public event Action<string>? OnSearchChanged;
|
||||||
|
public event Action? OnInventoryOpened;
|
||||||
|
public event Action? OnInventoryClosed;
|
||||||
|
public event Action? OnCategoriesRefreshed;
|
||||||
|
|
||||||
|
public bool IsInventoryOpen => System.AddonInventoryWindow?.IsOpen ?? false;
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> GetVisibleItemIds()
|
||||||
|
{
|
||||||
|
var window = System.AddonInventoryWindow;
|
||||||
|
if (window == null || !window.IsOpen) return Array.Empty<uint>();
|
||||||
|
|
||||||
|
var categories = window.GetVisibleCategories();
|
||||||
|
if (categories == null) return Array.Empty<uint>();
|
||||||
|
|
||||||
|
var result = new List<uint>();
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
foreach (var item in category.Items)
|
||||||
|
{
|
||||||
|
result.Add(item.Item.ItemId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<uint> GetItemsInCategory(uint categoryKey)
|
||||||
|
{
|
||||||
|
var window = System.AddonInventoryWindow;
|
||||||
|
if (window == null || !window.IsOpen) return Array.Empty<uint>();
|
||||||
|
|
||||||
|
var categories = window.GetVisibleCategories();
|
||||||
|
if (categories == null) return Array.Empty<uint>();
|
||||||
|
|
||||||
|
var category = categories.FirstOrDefault(c => c.Key == categoryKey);
|
||||||
|
if (category.Items == null) return Array.Empty<uint>();
|
||||||
|
|
||||||
|
return category.Items.Select(i => i.Item.ItemId).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsItemVisible(uint itemId)
|
||||||
|
{
|
||||||
|
var window = System.AddonInventoryWindow;
|
||||||
|
if (window == null || !window.IsOpen) return false;
|
||||||
|
|
||||||
|
var categories = window.GetVisibleCategories();
|
||||||
|
if (categories == null) return false;
|
||||||
|
|
||||||
|
foreach (var category in categories)
|
||||||
|
{
|
||||||
|
if (category.Items.Any(i => i.Item.ItemId == itemId))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string GetCurrentSearchFilter()
|
||||||
|
{
|
||||||
|
return System.AddonInventoryWindow?.GetSearchText() ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterSource(IExternalItemSource source)
|
||||||
|
{
|
||||||
|
ExternalCategoryManager.RegisterSource(source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UnregisterSource(string sourceName)
|
||||||
|
{
|
||||||
|
ExternalCategoryManager.UnregisterSource(sourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<string> GetRegisteredSourceNames()
|
||||||
|
{
|
||||||
|
return ExternalCategoryManager.RegisteredSources.Select(s => s.SourceName).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RaiseItemHovered(uint itemId) => OnItemHovered?.Invoke(itemId);
|
||||||
|
public void RaiseItemUnhovered(uint itemId) => OnItemUnhovered?.Invoke(itemId);
|
||||||
|
public void RaiseItemClicked(uint itemId) => OnItemClicked?.Invoke(itemId);
|
||||||
|
public void RaiseSearchChanged(string search) => OnSearchChanged?.Invoke(search);
|
||||||
|
public void RaiseInventoryOpened() => OnInventoryOpened?.Invoke();
|
||||||
|
public void RaiseInventoryClosed() => OnInventoryClosed?.Invoke();
|
||||||
|
public void RaiseCategoriesRefreshed() => OnCategoriesRefreshed?.Invoke();
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Dalamud.Plugin.Ipc;
|
||||||
|
|
||||||
|
namespace AetherBags.IPC.AetherBagsAPI;
|
||||||
|
|
||||||
|
public class AetherBagsIPCProvider : IDisposable
|
||||||
|
{
|
||||||
|
private const string IpcPrefix = "AetherBags.";
|
||||||
|
|
||||||
|
private readonly AetherBagsAPIImpl _api;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<bool> _isInventoryOpen;
|
||||||
|
private readonly ICallGateProvider<List<uint>> _getVisibleItemIds;
|
||||||
|
private readonly ICallGateProvider<uint, List<uint>> _getItemsInCategory;
|
||||||
|
private readonly ICallGateProvider<uint, bool> _isItemVisible;
|
||||||
|
private readonly ICallGateProvider<string> _getSearchFilter;
|
||||||
|
private readonly ICallGateProvider<List<string>> _getRegisteredSources;
|
||||||
|
|
||||||
|
private readonly ICallGateProvider<uint, bool> _onItemHovered;
|
||||||
|
private readonly ICallGateProvider<uint, bool> _onItemUnhovered;
|
||||||
|
private readonly ICallGateProvider<uint, bool> _onItemClicked;
|
||||||
|
private readonly ICallGateProvider<string, bool> _onSearchChanged;
|
||||||
|
private readonly ICallGateProvider<bool> _onInventoryOpened;
|
||||||
|
private readonly ICallGateProvider<bool> _onInventoryClosed;
|
||||||
|
private readonly ICallGateProvider<bool> _onCategoriesRefreshed;
|
||||||
|
|
||||||
|
public AetherBagsAPIImpl API => _api;
|
||||||
|
|
||||||
|
public AetherBagsIPCProvider()
|
||||||
|
{
|
||||||
|
_api = new AetherBagsAPIImpl();
|
||||||
|
|
||||||
|
_isInventoryOpen = Services.PluginInterface.GetIpcProvider<bool>($"{IpcPrefix}IsInventoryOpen");
|
||||||
|
_getVisibleItemIds = Services.PluginInterface.GetIpcProvider<List<uint>>($"{IpcPrefix}GetVisibleItemIds");
|
||||||
|
_getItemsInCategory = Services.PluginInterface.GetIpcProvider<uint, List<uint>>($"{IpcPrefix}GetItemsInCategory");
|
||||||
|
_isItemVisible = Services.PluginInterface.GetIpcProvider<uint, bool>($"{IpcPrefix}IsItemVisible");
|
||||||
|
_getSearchFilter = Services.PluginInterface.GetIpcProvider<string>($"{IpcPrefix}GetSearchFilter");
|
||||||
|
_getRegisteredSources = Services.PluginInterface.GetIpcProvider<List<string>>($"{IpcPrefix}GetRegisteredSources");
|
||||||
|
|
||||||
|
_onItemHovered = Services.PluginInterface.GetIpcProvider<uint, bool>($"{IpcPrefix}OnItemHovered");
|
||||||
|
_onItemUnhovered = Services.PluginInterface.GetIpcProvider<uint, bool>($"{IpcPrefix}OnItemUnhovered");
|
||||||
|
_onItemClicked = Services.PluginInterface.GetIpcProvider<uint, bool>($"{IpcPrefix}OnItemClicked");
|
||||||
|
_onSearchChanged = Services.PluginInterface.GetIpcProvider<string, bool>($"{IpcPrefix}OnSearchChanged");
|
||||||
|
_onInventoryOpened = Services.PluginInterface.GetIpcProvider<bool>($"{IpcPrefix}OnInventoryOpened");
|
||||||
|
_onInventoryClosed = Services.PluginInterface.GetIpcProvider<bool>($"{IpcPrefix}OnInventoryClosed");
|
||||||
|
_onCategoriesRefreshed = Services.PluginInterface.GetIpcProvider<bool>($"{IpcPrefix}OnCategoriesRefreshed");
|
||||||
|
|
||||||
|
RegisterFunctions();
|
||||||
|
SubscribeEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterFunctions()
|
||||||
|
{
|
||||||
|
_isInventoryOpen.RegisterFunc(() => _api.IsInventoryOpen);
|
||||||
|
_getVisibleItemIds.RegisterFunc(() => new List<uint>(_api.GetVisibleItemIds()));
|
||||||
|
_getItemsInCategory.RegisterFunc(key => new List<uint>(_api.GetItemsInCategory(key)));
|
||||||
|
_isItemVisible.RegisterFunc(itemId => _api.IsItemVisible(itemId));
|
||||||
|
_getSearchFilter.RegisterFunc(() => _api.GetCurrentSearchFilter());
|
||||||
|
_getRegisteredSources.RegisterFunc(() => new List<string>(_api.GetRegisteredSourceNames()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SubscribeEvents()
|
||||||
|
{
|
||||||
|
_api.OnItemHovered += itemId => _onItemHovered.SendMessage(itemId);
|
||||||
|
_api.OnItemUnhovered += itemId => _onItemUnhovered.SendMessage(itemId);
|
||||||
|
_api.OnItemClicked += itemId => _onItemClicked.SendMessage(itemId);
|
||||||
|
_api.OnSearchChanged += search => _onSearchChanged.SendMessage(search);
|
||||||
|
_api.OnInventoryOpened += () => _onInventoryOpened.SendMessage();
|
||||||
|
_api.OnInventoryClosed += () => _onInventoryClosed.SendMessage();
|
||||||
|
_api.OnCategoriesRefreshed += () => _onCategoriesRefreshed.SendMessage();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_isInventoryOpen.UnregisterFunc();
|
||||||
|
_getVisibleItemIds.UnregisterFunc();
|
||||||
|
_getItemsInCategory.UnregisterFunc();
|
||||||
|
_isItemVisible.UnregisterFunc();
|
||||||
|
_getSearchFilter.UnregisterFunc();
|
||||||
|
_getRegisteredSources.UnregisterFunc();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
|
|
||||||
|
namespace AetherBags.IPC.AetherBagsAPI;
|
||||||
|
|
||||||
|
public interface IAetherBagsAPI
|
||||||
|
{
|
||||||
|
IReadOnlyList<uint> GetVisibleItemIds();
|
||||||
|
IReadOnlyList<uint> GetItemsInCategory(uint categoryKey);
|
||||||
|
bool IsItemVisible(uint itemId);
|
||||||
|
string GetCurrentSearchFilter();
|
||||||
|
bool IsInventoryOpen { get; }
|
||||||
|
|
||||||
|
event Action<uint>? OnItemHovered;
|
||||||
|
event Action<uint>? OnItemUnhovered;
|
||||||
|
event Action<uint>? OnItemClicked;
|
||||||
|
event Action<string>? OnSearchChanged;
|
||||||
|
event Action? OnInventoryOpened;
|
||||||
|
event Action? OnInventoryClosed;
|
||||||
|
event Action? OnCategoriesRefreshed;
|
||||||
|
|
||||||
|
void RegisterSource(IExternalItemSource source);
|
||||||
|
void UnregisterSource(string sourceName);
|
||||||
|
IReadOnlyList<string> GetRegisteredSourceNames();
|
||||||
|
}
|
||||||
@@ -1,8 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using KamiToolKit.Classes;
|
||||||
|
|
||||||
namespace AetherBags.IPC;
|
namespace AetherBags.IPC;
|
||||||
|
|
||||||
@@ -188,8 +192,119 @@ public class AllaganToolsIPC : IDisposable
|
|||||||
InventoryOrchestrator.RefreshHighlights();
|
InventoryOrchestrator.RefreshHighlights();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private AllaganToolsSource? _source;
|
||||||
|
|
||||||
|
public void EnableExternalCategorySupport()
|
||||||
|
{
|
||||||
|
if (_source != null) return;
|
||||||
|
|
||||||
|
_source = new AllaganToolsSource(this);
|
||||||
|
ExternalCategoryManager.RegisterSource(_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisableExternalCategorySupport()
|
||||||
|
{
|
||||||
|
if (_source == null) return;
|
||||||
|
|
||||||
|
ExternalCategoryManager.UnregisterSource(_source.SourceName);
|
||||||
|
_source = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
DisableExternalCategorySupport();
|
||||||
_initialized?.Unsubscribe(OnAllaganInitialized);
|
_initialized?.Unsubscribe(OnAllaganInitialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class AllaganToolsSource : IExternalItemSource
|
||||||
|
{
|
||||||
|
private readonly AllaganToolsIPC _ipc;
|
||||||
|
private int _version;
|
||||||
|
|
||||||
|
public string SourceName => "AllaganTools";
|
||||||
|
public string DisplayName => "Allagan Tools";
|
||||||
|
public int Priority => 50;
|
||||||
|
public bool IsReady => _ipc.IsReady;
|
||||||
|
public int Version => _version;
|
||||||
|
public event Action? OnDataChanged;
|
||||||
|
|
||||||
|
public SourceCapabilities Capabilities =>
|
||||||
|
SourceCapabilities.Categories |
|
||||||
|
SourceCapabilities.SearchTags;
|
||||||
|
|
||||||
|
public ConflictBehavior ConflictBehavior => ConflictBehavior.Defer;
|
||||||
|
|
||||||
|
public AllaganToolsSource(AllaganToolsIPC ipc)
|
||||||
|
{
|
||||||
|
_ipc = ipc;
|
||||||
|
_ipc.OnFiltersRefreshed += OnIpcRefreshed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIpcRefreshed()
|
||||||
|
{
|
||||||
|
_version++;
|
||||||
|
OnDataChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, ExternalCategoryAssignment>? GetCategoryAssignments()
|
||||||
|
{
|
||||||
|
if (_ipc.CachedFilterItems.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<uint, ExternalCategoryAssignment>();
|
||||||
|
int filterIndex = 0;
|
||||||
|
|
||||||
|
foreach (var (filterKey, filterName) in _ipc.CachedSearchFilters)
|
||||||
|
{
|
||||||
|
if (!_ipc.CachedFilterItems.TryGetValue(filterKey, out var itemIds))
|
||||||
|
{
|
||||||
|
filterIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint categoryKey = CategoryBucketManager.MakeAllaganFilterKey(filterIndex);
|
||||||
|
|
||||||
|
foreach (var itemId in itemIds.Keys)
|
||||||
|
{
|
||||||
|
result.TryAdd(itemId, new ExternalCategoryAssignment(
|
||||||
|
CategoryKey: categoryKey,
|
||||||
|
CategoryName: $"[AT] {filterName}",
|
||||||
|
CategoryDescription: $"Allagan Tools filter: {filterName}",
|
||||||
|
CategoryColor: ColorHelper.GetColor(32),
|
||||||
|
ItemOverlayColor: null,
|
||||||
|
SubPriority: filterIndex
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
filterIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, ItemDecoration>? GetItemDecorations() => null;
|
||||||
|
|
||||||
|
public IReadOnlyList<ContextMenuEntry>? GetContextMenuEntries(uint itemId) => null;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, string[]>? GetSearchTags()
|
||||||
|
{
|
||||||
|
if (_ipc.ItemToFilters.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<uint, string[]>();
|
||||||
|
foreach (var (itemId, filterKeys) in _ipc.ItemToFilters)
|
||||||
|
{
|
||||||
|
var tags = new List<string>(filterKeys.Count + 1) { "at", "allagantools" };
|
||||||
|
foreach (var key in filterKeys)
|
||||||
|
{
|
||||||
|
if (_ipc.CachedSearchFilters.TryGetValue(key, out var name))
|
||||||
|
{
|
||||||
|
tags.Add(name.ToLowerInvariant());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result[itemId] = tags.ToArray();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ItemRelationship>? GetItemRelationships(uint itemId) => null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,8 +2,11 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
using Dalamud.Plugin.Ipc;
|
using Dalamud.Plugin.Ipc;
|
||||||
|
using KamiToolKit.Classes;
|
||||||
|
|
||||||
namespace AetherBags.IPC;
|
namespace AetherBags.IPC;
|
||||||
|
|
||||||
@@ -190,9 +193,157 @@ public class BisBuddyIPC : IDisposable
|
|||||||
public Vector4? GetItemColor(uint itemId)
|
public Vector4? GetItemColor(uint itemId)
|
||||||
=> GetBisItem(itemId)?.Color;
|
=> GetBisItem(itemId)?.Color;
|
||||||
|
|
||||||
|
private BisBuddySource? _source;
|
||||||
|
|
||||||
|
public void EnableExternalCategorySupport()
|
||||||
|
{
|
||||||
|
if (_source != null) return;
|
||||||
|
|
||||||
|
_source = new BisBuddySource(this);
|
||||||
|
ExternalCategoryManager.RegisterSource(_source);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void DisableExternalCategorySupport()
|
||||||
|
{
|
||||||
|
if (_source == null) return;
|
||||||
|
|
||||||
|
ExternalCategoryManager.UnregisterSource(_source.SourceName);
|
||||||
|
_source = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
DisableExternalCategorySupport();
|
||||||
_initialized?.Unsubscribe(OnBisBuddyInitialized);
|
_initialized?.Unsubscribe(OnBisBuddyInitialized);
|
||||||
_inventoryHighlightItemsChanged?.Unsubscribe(OnInventoryHighlightItemsChanged);
|
_inventoryHighlightItemsChanged?.Unsubscribe(OnInventoryHighlightItemsChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class BisBuddySource : IExternalItemSource
|
||||||
|
{
|
||||||
|
private readonly BisBuddyIPC _ipc;
|
||||||
|
private int _version;
|
||||||
|
|
||||||
|
public string SourceName => "BisBuddy";
|
||||||
|
public string DisplayName => "Best in Slot";
|
||||||
|
public int Priority => 100;
|
||||||
|
public bool IsReady => _ipc.IsReady;
|
||||||
|
public int Version => _version;
|
||||||
|
public event Action? OnDataChanged;
|
||||||
|
|
||||||
|
public SourceCapabilities Capabilities =>
|
||||||
|
SourceCapabilities.Categories |
|
||||||
|
SourceCapabilities.ItemColors |
|
||||||
|
SourceCapabilities.SearchTags |
|
||||||
|
SourceCapabilities.Relationships;
|
||||||
|
|
||||||
|
public ConflictBehavior ConflictBehavior => ConflictBehavior.Replace;
|
||||||
|
|
||||||
|
public BisBuddySource(BisBuddyIPC ipc)
|
||||||
|
{
|
||||||
|
_ipc = ipc;
|
||||||
|
_ipc.OnItemsRefreshed += OnIpcRefreshed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnIpcRefreshed()
|
||||||
|
{
|
||||||
|
_version++;
|
||||||
|
OnDataChanged?.Invoke();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, ExternalCategoryAssignment>? GetCategoryAssignments()
|
||||||
|
{
|
||||||
|
var items = _ipc.ItemLookup;
|
||||||
|
if (items.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<uint, ExternalCategoryAssignment>();
|
||||||
|
|
||||||
|
var colorGroups = new Dictionary<Vector4, List<(uint itemId, BisItemEntry entry)>>();
|
||||||
|
foreach (var (itemId, entry) in items)
|
||||||
|
{
|
||||||
|
if (!colorGroups.TryGetValue(entry.Color, out var list))
|
||||||
|
{
|
||||||
|
list = new List<(uint, BisItemEntry)>();
|
||||||
|
colorGroups[entry.Color] = list;
|
||||||
|
}
|
||||||
|
list.Add((itemId, entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
uint subKey = 0;
|
||||||
|
foreach (var (color, groupItems) in colorGroups)
|
||||||
|
{
|
||||||
|
uint categoryKey = CategoryBucketManager.MakeBisBuddyKey() | subKey++;
|
||||||
|
|
||||||
|
foreach (var (itemId, entry) in groupItems)
|
||||||
|
{
|
||||||
|
result[itemId] = new ExternalCategoryAssignment(
|
||||||
|
CategoryKey: categoryKey,
|
||||||
|
CategoryName: "[BiS] Gearset",
|
||||||
|
CategoryDescription: "Items needed for Best in Slot",
|
||||||
|
CategoryColor: color,
|
||||||
|
ItemOverlayColor: new Vector3(color.X, color.Y, color.Z),
|
||||||
|
SubPriority: 0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, ItemDecoration>? GetItemDecorations()
|
||||||
|
{
|
||||||
|
var items = _ipc.ItemLookup;
|
||||||
|
if (items.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<uint, ItemDecoration>();
|
||||||
|
foreach (var (itemId, entry) in items)
|
||||||
|
{
|
||||||
|
result[itemId] = new ItemDecoration
|
||||||
|
{
|
||||||
|
OverlayColor = new Vector3(entry.Color.X, entry.Color.Y, entry.Color.Z),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ContextMenuEntry>? GetContextMenuEntries(uint itemId) => null;
|
||||||
|
|
||||||
|
public IReadOnlyDictionary<uint, string[]>? GetSearchTags()
|
||||||
|
{
|
||||||
|
var items = _ipc.ItemLookup;
|
||||||
|
if (items.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new Dictionary<uint, string[]>();
|
||||||
|
foreach (var itemId in items.Keys)
|
||||||
|
{
|
||||||
|
result[itemId] = new[] { "bis", "bestinslot", "gearset" };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IReadOnlyList<ItemRelationship>? GetItemRelationships(uint itemId)
|
||||||
|
{
|
||||||
|
if (!_ipc.ItemLookup.TryGetValue(itemId, out var entry)) return null;
|
||||||
|
|
||||||
|
var sameSetItems = new List<uint>();
|
||||||
|
foreach (var (otherId, otherEntry) in _ipc.ItemLookup)
|
||||||
|
{
|
||||||
|
if (otherId != itemId && otherEntry.Color == entry.Color)
|
||||||
|
{
|
||||||
|
sameSetItems.Add(otherId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sameSetItems.Count == 0) return null;
|
||||||
|
|
||||||
|
return new[]
|
||||||
|
{
|
||||||
|
new ItemRelationship(
|
||||||
|
Type: RelationshipType.SameSet,
|
||||||
|
RelatedItemIds: sameSetItems.ToArray(),
|
||||||
|
GroupLabel: "Same Gearset",
|
||||||
|
HighlightColor: new Vector3(entry.Color.X, entry.Color.Y, entry.Color.Z)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,297 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Numerics;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using AetherBags.Inventory.Categories;
|
||||||
|
using AetherBags.Inventory.Items;
|
||||||
|
|
||||||
|
namespace AetherBags.IPC.ExternalCategorySystem;
|
||||||
|
|
||||||
|
public static class ExternalCategoryManager
|
||||||
|
{
|
||||||
|
private static readonly List<IExternalItemSource> Sources = new();
|
||||||
|
private static readonly Dictionary<uint, ExternalCategoryAssignment> CategoryCache = new();
|
||||||
|
private static readonly Dictionary<uint, ItemDecoration> DecorationCache = new();
|
||||||
|
private static readonly Dictionary<uint, List<string>> SearchTagCache = new();
|
||||||
|
private static int _lastCombinedVersion;
|
||||||
|
|
||||||
|
public static IReadOnlyList<IExternalItemSource> RegisteredSources => Sources;
|
||||||
|
|
||||||
|
public static void RegisterSource(IExternalItemSource source)
|
||||||
|
{
|
||||||
|
if (Sources.Any(s => s.SourceName == source.SourceName))
|
||||||
|
return;
|
||||||
|
|
||||||
|
Sources.Add(source);
|
||||||
|
Sources.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||||
|
source.OnDataChanged += InvalidateCache;
|
||||||
|
InvalidateCache();
|
||||||
|
|
||||||
|
Services.Logger.Information($"Registered external category source: {source.SourceName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void UnregisterSource(string sourceName)
|
||||||
|
{
|
||||||
|
var source = Sources.FirstOrDefault(s => s.SourceName == sourceName);
|
||||||
|
if (source == null) return;
|
||||||
|
|
||||||
|
source.OnDataChanged -= InvalidateCache;
|
||||||
|
Sources.Remove(source);
|
||||||
|
InvalidateCache();
|
||||||
|
|
||||||
|
Services.Logger.Information($"Unregistered external category source: {sourceName}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InvalidateCache()
|
||||||
|
{
|
||||||
|
_lastCombinedVersion = -1;
|
||||||
|
CategoryCache.Clear();
|
||||||
|
DecorationCache.Clear();
|
||||||
|
SearchTagCache.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ComputeCombinedVersion()
|
||||||
|
{
|
||||||
|
int version = 0;
|
||||||
|
foreach (var source in Sources)
|
||||||
|
version = unchecked(version * 31 + source.Version);
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RebuildCacheIfNeeded()
|
||||||
|
{
|
||||||
|
int currentVersion = ComputeCombinedVersion();
|
||||||
|
if (currentVersion == _lastCombinedVersion && CategoryCache.Count > 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_lastCombinedVersion = currentVersion;
|
||||||
|
CategoryCache.Clear();
|
||||||
|
DecorationCache.Clear();
|
||||||
|
SearchTagCache.Clear();
|
||||||
|
|
||||||
|
foreach (var source in Sources)
|
||||||
|
{
|
||||||
|
if (!source.IsReady) continue;
|
||||||
|
|
||||||
|
if (source.Capabilities.HasFlag(SourceCapabilities.Categories))
|
||||||
|
{
|
||||||
|
var categories = source.GetCategoryAssignments();
|
||||||
|
if (categories != null)
|
||||||
|
{
|
||||||
|
foreach (var (itemId, assignment) in categories)
|
||||||
|
{
|
||||||
|
CategoryCache.TryAdd(itemId, assignment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.Capabilities.HasFlag(SourceCapabilities.ItemColors) ||
|
||||||
|
source.Capabilities.HasFlag(SourceCapabilities.Badges))
|
||||||
|
{
|
||||||
|
var decorations = source.GetItemDecorations();
|
||||||
|
if (decorations != null)
|
||||||
|
{
|
||||||
|
foreach (var (itemId, decoration) in decorations)
|
||||||
|
{
|
||||||
|
if (DecorationCache.TryGetValue(itemId, out var existing))
|
||||||
|
{
|
||||||
|
DecorationCache[itemId] = MergeDecorations(existing, decoration, source.ConflictBehavior);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DecorationCache[itemId] = decoration;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (source.Capabilities.HasFlag(SourceCapabilities.SearchTags))
|
||||||
|
{
|
||||||
|
var searchTags = source.GetSearchTags();
|
||||||
|
if (searchTags != null)
|
||||||
|
{
|
||||||
|
foreach (var (itemId, tags) in searchTags)
|
||||||
|
{
|
||||||
|
if (!SearchTagCache.TryGetValue(itemId, out var existingTags))
|
||||||
|
{
|
||||||
|
existingTags = new List<string>(tags.Length);
|
||||||
|
SearchTagCache[itemId] = existingTags;
|
||||||
|
}
|
||||||
|
existingTags.AddRange(tags);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ItemDecoration MergeDecorations(ItemDecoration existing, ItemDecoration incoming, ConflictBehavior behavior)
|
||||||
|
{
|
||||||
|
return behavior switch
|
||||||
|
{
|
||||||
|
ConflictBehavior.Replace => incoming,
|
||||||
|
ConflictBehavior.Defer => existing,
|
||||||
|
ConflictBehavior.Merge => new ItemDecoration
|
||||||
|
{
|
||||||
|
OverlayColor = incoming.OverlayColor ?? existing.OverlayColor,
|
||||||
|
Opacity = incoming.Opacity ?? existing.Opacity,
|
||||||
|
Badge = incoming.Badge ?? existing.Badge,
|
||||||
|
Border = incoming.Border != BorderStyle.None ? incoming.Border : existing.Border,
|
||||||
|
TooltipLine = CombineTooltips(existing.TooltipLine, incoming.TooltipLine),
|
||||||
|
},
|
||||||
|
_ => incoming
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? CombineTooltips(string? a, string? b)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(a)) return b;
|
||||||
|
if (string.IsNullOrEmpty(b)) return a;
|
||||||
|
return $"{a}\n{b}";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void BucketItems(
|
||||||
|
Dictionary<ulong, ItemInfo> itemInfoByKey,
|
||||||
|
Dictionary<uint, CategoryBucket> bucketsByKey,
|
||||||
|
HashSet<ulong> claimedKeys)
|
||||||
|
{
|
||||||
|
RebuildCacheIfNeeded();
|
||||||
|
|
||||||
|
if (CategoryCache.Count == 0) return;
|
||||||
|
|
||||||
|
foreach (var (itemKey, item) in itemInfoByKey)
|
||||||
|
{
|
||||||
|
if (claimedKeys.Contains(itemKey)) continue;
|
||||||
|
|
||||||
|
if (!CategoryCache.TryGetValue(item.Item.ItemId, out var assignment))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
ref var bucketRef = ref CollectionsMarshal.GetValueRefOrAddDefault(bucketsByKey, assignment.CategoryKey, out bool exists);
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
bucketRef = new CategoryBucket
|
||||||
|
{
|
||||||
|
Key = assignment.CategoryKey,
|
||||||
|
Category = new CategoryInfo
|
||||||
|
{
|
||||||
|
Name = assignment.CategoryName,
|
||||||
|
Description = assignment.CategoryDescription ?? string.Empty,
|
||||||
|
Color = assignment.CategoryColor,
|
||||||
|
},
|
||||||
|
Items = new List<ItemInfo>(16),
|
||||||
|
FilteredItems = new List<ItemInfo>(16),
|
||||||
|
Used = true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bucketRef!.Used = true;
|
||||||
|
bucketRef.Category.Name = assignment.CategoryName;
|
||||||
|
bucketRef.Category.Description = assignment.CategoryDescription ?? string.Empty;
|
||||||
|
bucketRef.Category.Color = assignment.CategoryColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
bucketRef!.Items.Add(item);
|
||||||
|
claimedKeys.Add(itemKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ItemDecoration? GetDecoration(uint itemId)
|
||||||
|
{
|
||||||
|
RebuildCacheIfNeeded();
|
||||||
|
return DecorationCache.TryGetValue(itemId, out var dec) ? dec : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Vector3? GetItemOverlayColor(uint itemId)
|
||||||
|
{
|
||||||
|
if (CategoryCache.TryGetValue(itemId, out var assignment))
|
||||||
|
return assignment.ItemOverlayColor;
|
||||||
|
|
||||||
|
if (DecorationCache.TryGetValue(itemId, out var decoration))
|
||||||
|
return decoration.OverlayColor;
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ContextMenuEntry>? GetContextMenuEntries(uint itemId)
|
||||||
|
{
|
||||||
|
List<ContextMenuEntry>? result = null;
|
||||||
|
|
||||||
|
foreach (var source in Sources)
|
||||||
|
{
|
||||||
|
if (!source.IsReady) continue;
|
||||||
|
if (!source.Capabilities.HasFlag(SourceCapabilities.ContextMenu)) continue;
|
||||||
|
|
||||||
|
var entries = source.GetContextMenuEntries(itemId);
|
||||||
|
if (entries == null || entries.Count == 0) continue;
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
if (entry.IsVisible != null && !entry.IsVisible(itemId)) continue;
|
||||||
|
|
||||||
|
result ??= new List<ContextMenuEntry>(4);
|
||||||
|
result.Add(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result?.Sort((a, b) => a.Order.CompareTo(b.Order));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IReadOnlyList<string>? GetSearchTags(uint itemId)
|
||||||
|
{
|
||||||
|
RebuildCacheIfNeeded();
|
||||||
|
return SearchTagCache.TryGetValue(itemId, out var tags) ? tags : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool MatchesSearchTag(uint itemId, string searchText)
|
||||||
|
{
|
||||||
|
RebuildCacheIfNeeded();
|
||||||
|
if (!SearchTagCache.TryGetValue(itemId, out var tags)) return false;
|
||||||
|
|
||||||
|
foreach (var tag in tags)
|
||||||
|
{
|
||||||
|
if (tag.Contains(searchText, global::System.StringComparison.OrdinalIgnoreCase))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<ItemRelationship>? GetItemRelationships(uint itemId)
|
||||||
|
{
|
||||||
|
List<ItemRelationship>? result = null;
|
||||||
|
|
||||||
|
foreach (var source in Sources)
|
||||||
|
{
|
||||||
|
if (!source.IsReady) continue;
|
||||||
|
if (!source.Capabilities.HasFlag(SourceCapabilities.Relationships)) continue;
|
||||||
|
|
||||||
|
var relationships = source.GetItemRelationships(itemId);
|
||||||
|
if (relationships == null || relationships.Count == 0) continue;
|
||||||
|
|
||||||
|
result ??= new List<ItemRelationship>(4);
|
||||||
|
result.AddRange(relationships);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HashSet<uint>? GetRelatedItemIds(uint itemId, RelationshipType? filterType = null)
|
||||||
|
{
|
||||||
|
var relationships = GetItemRelationships(itemId);
|
||||||
|
if (relationships == null || relationships.Count == 0) return null;
|
||||||
|
|
||||||
|
var result = new HashSet<uint>();
|
||||||
|
foreach (var rel in relationships)
|
||||||
|
{
|
||||||
|
if (filterType.HasValue && rel.Type != filterType.Value) continue;
|
||||||
|
|
||||||
|
foreach (var relatedId in rel.RelatedItemIds)
|
||||||
|
{
|
||||||
|
result.Add(relatedId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Count > 0 ? result : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Numerics;
|
||||||
|
|
||||||
|
namespace AetherBags.IPC.ExternalCategorySystem;
|
||||||
|
|
||||||
|
public interface IExternalItemSource
|
||||||
|
{
|
||||||
|
string SourceName { get; }
|
||||||
|
string DisplayName { get; }
|
||||||
|
int Priority { get; }
|
||||||
|
bool IsReady { get; }
|
||||||
|
|
||||||
|
int Version { get; }
|
||||||
|
event Action? OnDataChanged;
|
||||||
|
|
||||||
|
SourceCapabilities Capabilities { get; }
|
||||||
|
ConflictBehavior ConflictBehavior { get; }
|
||||||
|
|
||||||
|
IReadOnlyDictionary<uint, ExternalCategoryAssignment>? GetCategoryAssignments();
|
||||||
|
IReadOnlyDictionary<uint, ItemDecoration>? GetItemDecorations();
|
||||||
|
IReadOnlyList<ContextMenuEntry>? GetContextMenuEntries(uint itemId);
|
||||||
|
IReadOnlyDictionary<uint, string[]>? GetSearchTags();
|
||||||
|
IReadOnlyList<ItemRelationship>? GetItemRelationships(uint itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum SourceCapabilities
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
Categories = 1,
|
||||||
|
ItemColors = 2,
|
||||||
|
Badges = 4,
|
||||||
|
ContextMenu = 8,
|
||||||
|
SearchTags = 16,
|
||||||
|
Relationships = 32,
|
||||||
|
Tooltips = 64
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ConflictBehavior
|
||||||
|
{
|
||||||
|
Replace,
|
||||||
|
Merge,
|
||||||
|
Defer
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly record struct ExternalCategoryAssignment(
|
||||||
|
uint CategoryKey,
|
||||||
|
string CategoryName,
|
||||||
|
string? CategoryDescription,
|
||||||
|
Vector4 CategoryColor,
|
||||||
|
Vector3? ItemOverlayColor,
|
||||||
|
int SubPriority
|
||||||
|
);
|
||||||
|
|
||||||
|
public record struct ItemDecoration
|
||||||
|
{
|
||||||
|
public Vector3? OverlayColor { get; init; }
|
||||||
|
public float? Opacity { get; init; }
|
||||||
|
public BadgeInfo? Badge { get; init; }
|
||||||
|
public BorderStyle Border { get; init; }
|
||||||
|
public string? TooltipLine { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public record struct BadgeInfo(
|
||||||
|
uint IconId,
|
||||||
|
BadgePosition Position,
|
||||||
|
Vector4? TintColor
|
||||||
|
);
|
||||||
|
|
||||||
|
public enum BadgePosition { TopLeft, TopRight, BottomLeft, BottomRight }
|
||||||
|
public enum BorderStyle { None, Solid, Glow, Pulse }
|
||||||
|
|
||||||
|
public record struct ContextMenuEntry(
|
||||||
|
string Label,
|
||||||
|
uint? IconId,
|
||||||
|
Action<ContextMenuContext> OnClick,
|
||||||
|
int Order,
|
||||||
|
Func<uint, bool>? IsVisible = null
|
||||||
|
);
|
||||||
|
|
||||||
|
public record struct ContextMenuContext(
|
||||||
|
uint ItemId,
|
||||||
|
int Container,
|
||||||
|
int Slot
|
||||||
|
);
|
||||||
|
|
||||||
|
public record struct ItemRelationship(
|
||||||
|
RelationshipType Type,
|
||||||
|
uint[] RelatedItemIds,
|
||||||
|
string? GroupLabel,
|
||||||
|
Vector3? HighlightColor
|
||||||
|
);
|
||||||
|
|
||||||
|
public enum RelationshipType
|
||||||
|
{
|
||||||
|
SameSet,
|
||||||
|
Upgrades,
|
||||||
|
UpgradedFrom,
|
||||||
|
CraftedFrom,
|
||||||
|
CraftsInto,
|
||||||
|
Alternative
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using AetherBags.Configuration;
|
||||||
|
|
||||||
namespace AetherBags.IPC;
|
namespace AetherBags.IPC;
|
||||||
|
|
||||||
@@ -8,6 +9,42 @@ public class IPCService : IDisposable
|
|||||||
public WotsItIPC WotsIt { get; } = new();
|
public WotsItIPC WotsIt { get; } = new();
|
||||||
public BisBuddyIPC BisBuddy { get; } = new();
|
public BisBuddyIPC BisBuddy { get; } = new();
|
||||||
|
|
||||||
|
private bool _unifiedEnabled;
|
||||||
|
|
||||||
|
public void UpdateUnifiedCategorySupport(bool enabled)
|
||||||
|
{
|
||||||
|
_unifiedEnabled = enabled;
|
||||||
|
RefreshExternalSources();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RefreshExternalSources()
|
||||||
|
{
|
||||||
|
var config = System.Config?.Categories;
|
||||||
|
if (config == null) return;
|
||||||
|
|
||||||
|
bool categoriesEnabled = config.CategoriesEnabled;
|
||||||
|
|
||||||
|
bool allaganShouldBeActive = _unifiedEnabled &&
|
||||||
|
categoriesEnabled &&
|
||||||
|
config.AllaganToolsCategoriesEnabled &&
|
||||||
|
config.AllaganToolsFilterMode == PluginFilterMode.Categorize;
|
||||||
|
|
||||||
|
if (allaganShouldBeActive)
|
||||||
|
AllaganTools.EnableExternalCategorySupport();
|
||||||
|
else
|
||||||
|
AllaganTools.DisableExternalCategorySupport();
|
||||||
|
|
||||||
|
bool bisBuddyShouldBeActive = _unifiedEnabled &&
|
||||||
|
categoriesEnabled &&
|
||||||
|
config.BisBuddyEnabled &&
|
||||||
|
config.BisBuddyMode == PluginFilterMode.Categorize;
|
||||||
|
|
||||||
|
if (bisBuddyShouldBeActive)
|
||||||
|
BisBuddy.EnableExternalCategorySupport();
|
||||||
|
else
|
||||||
|
BisBuddy.DisableExternalCategorySupport();
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
AllaganTools.Dispose();
|
AllaganTools.Dispose();
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
@@ -61,32 +63,24 @@ public static class CategoryBucketManager
|
|||||||
{
|
{
|
||||||
sortedScratch.Clear();
|
sortedScratch.Clear();
|
||||||
sortedScratch.AddRange(userCategories);
|
sortedScratch.AddRange(userCategories);
|
||||||
sortedScratch.Sort((left, right) =>
|
sortedScratch.Sort(UserCategoryComparer.Instance);
|
||||||
{
|
|
||||||
int priority = left.Priority.CompareTo(right.Priority);
|
|
||||||
if (priority != 0) return priority;
|
|
||||||
|
|
||||||
int order = left.Order.CompareTo(right.Order);
|
var activeBuckets = new (uint key, CategoryBucket bucket, UserCategoryDefinition def)[sortedScratch.Count];
|
||||||
if (order != 0) return order;
|
int activeCount = 0;
|
||||||
|
|
||||||
return string.Compare(left.Id, right.Id, StringComparison.OrdinalIgnoreCase);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (int i = 0; i < sortedScratch.Count; i++)
|
for (int i = 0; i < sortedScratch.Count; i++)
|
||||||
{
|
{
|
||||||
UserCategoryDefinition category = sortedScratch[i];
|
UserCategoryDefinition category = sortedScratch[i];
|
||||||
|
|
||||||
if (!category.Enabled)
|
if (!category.Enabled || UserCategoryMatcher.IsCatchAll(category))
|
||||||
continue;
|
|
||||||
|
|
||||||
if (UserCategoryMatcher.IsCatchAll(category))
|
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
uint bucketKey = MakeUserCategoryKey(category.Order);
|
uint bucketKey = MakeUserCategoryKey(category.Order);
|
||||||
|
ref var bucketRef = ref CollectionsMarshal.GetValueRefOrAddDefault(bucketsByKey, bucketKey, out bool exists);
|
||||||
|
|
||||||
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
|
if (!exists)
|
||||||
{
|
{
|
||||||
bucket = new CategoryBucket
|
bucketRef = new CategoryBucket
|
||||||
{
|
{
|
||||||
Key = bucketKey,
|
Key = bucketKey,
|
||||||
Category = new CategoryInfo
|
Category = new CategoryInfo
|
||||||
@@ -100,34 +94,63 @@ public static class CategoryBucketManager
|
|||||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||||
Used = true,
|
Used = true,
|
||||||
};
|
};
|
||||||
bucketsByKey.Add(bucketKey, bucket);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bucket.Used = true;
|
bucketRef!.Used = true;
|
||||||
bucket.Category.Name = category.Name;
|
bucketRef.Category.Name = category.Name;
|
||||||
bucket.Category.Description = category.Description;
|
bucketRef.Category.Description = category.Description;
|
||||||
bucket.Category.Color = category.Color;
|
bucketRef.Category.Color = category.Color;
|
||||||
bucket.Category.IsPinned = category.Pinned;
|
bucketRef.Category.IsPinned = category.Pinned;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeBuckets[activeCount++] = (bucketKey, bucketRef!, category);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var itemKvp in itemInfoByKey)
|
foreach (var itemKvp in itemInfoByKey)
|
||||||
{
|
{
|
||||||
ulong itemKey = itemKvp.Key;
|
ulong itemKey = itemKvp.Key;
|
||||||
ItemInfo item = itemKvp.Value;
|
|
||||||
|
|
||||||
if (claimedKeys.Contains(itemKey))
|
if (claimedKeys.Contains(itemKey))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (UserCategoryMatcher.Matches(item, category))
|
ItemInfo item = itemKvp.Value;
|
||||||
|
|
||||||
|
for (int i = 0; i < activeCount; i++)
|
||||||
{
|
{
|
||||||
bucket.Items.Add(item);
|
ref var entry = ref activeBuckets[i];
|
||||||
|
if (UserCategoryMatcher.Matches(item, entry.def))
|
||||||
|
{
|
||||||
|
entry.bucket.Items.Add(item);
|
||||||
claimedKeys.Add(itemKey);
|
claimedKeys.Add(itemKey);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (bucket.Items.Count == 0)
|
for (int i = 0; i < activeCount; i++)
|
||||||
bucket.Used = false;
|
{
|
||||||
|
ref var entry = ref activeBuckets[i];
|
||||||
|
if (entry.bucket.Items.Count == 0)
|
||||||
|
entry.bucket.Used = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class UserCategoryComparer : IComparer<UserCategoryDefinition>
|
||||||
|
{
|
||||||
|
public static readonly UserCategoryComparer Instance = new();
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public int Compare(UserCategoryDefinition? left, UserCategoryDefinition? right)
|
||||||
|
{
|
||||||
|
if (left is null || right is null) return 0;
|
||||||
|
|
||||||
|
int priority = left.Priority.CompareTo(right.Priority);
|
||||||
|
if (priority != 0) return priority;
|
||||||
|
|
||||||
|
int order = left.Order.CompareTo(right.Order);
|
||||||
|
if (order != 0) return order;
|
||||||
|
|
||||||
|
return string.Compare(left.Id, right.Id, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,9 +170,11 @@ public static class CategoryBucketManager
|
|||||||
|
|
||||||
uint categoryKey = info.UiCategory.RowId;
|
uint categoryKey = info.UiCategory.RowId;
|
||||||
|
|
||||||
if (!bucketsByKey.TryGetValue(categoryKey, out CategoryBucket? bucket))
|
ref var bucketRef = ref CollectionsMarshal.GetValueRefOrAddDefault(bucketsByKey, categoryKey, out bool exists);
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
{
|
{
|
||||||
bucket = new CategoryBucket
|
bucketRef = new CategoryBucket
|
||||||
{
|
{
|
||||||
Key = categoryKey,
|
Key = categoryKey,
|
||||||
Category = GetCategoryInfoCached(categoryKey, info),
|
Category = GetCategoryInfoCached(categoryKey, info),
|
||||||
@@ -157,14 +182,13 @@ public static class CategoryBucketManager
|
|||||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||||
Used = true,
|
Used = true,
|
||||||
};
|
};
|
||||||
bucketsByKey.Add(categoryKey, bucket);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bucket.Used = true;
|
bucketRef!.Used = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bucket.Items.Add(info);
|
bucketRef!.Items.Add(info);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,22 +202,26 @@ public static class CategoryBucketManager
|
|||||||
if (!System.IPC.AllaganTools.IsReady) return;
|
if (!System.IPC.AllaganTools.IsReady) return;
|
||||||
|
|
||||||
var filters = System.IPC.AllaganTools.CachedSearchFilters;
|
var filters = System.IPC.AllaganTools.CachedSearchFilters;
|
||||||
var filterItems = System.IPC.AllaganTools.CachedFilterItems;
|
var itemToFilters = System.IPC.AllaganTools.ItemToFilters;
|
||||||
|
|
||||||
|
if (filters.Count == 0 || itemToFilters.Count == 0) return;
|
||||||
|
|
||||||
|
var filterKeyToIndex = new Dictionary<string, int>(filters.Count);
|
||||||
int index = 0;
|
int index = 0;
|
||||||
foreach (var (filterKey, filterName) in filters)
|
foreach (var filterKey in filters.Keys)
|
||||||
{
|
{
|
||||||
if (!filterItems. TryGetValue(filterKey, out var itemIds))
|
filterKeyToIndex[filterKey] = index++;
|
||||||
{
|
|
||||||
index++;
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint bucketKey = MakeAllaganFilterKey(index);
|
index = 0;
|
||||||
|
foreach (var (filterKey, filterName) in filters)
|
||||||
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
|
|
||||||
{
|
{
|
||||||
bucket = new CategoryBucket
|
uint bucketKey = MakeAllaganFilterKey(index);
|
||||||
|
ref var bucketRef = ref CollectionsMarshal.GetValueRefOrAddDefault(bucketsByKey, bucketKey, out bool exists);
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
|
{
|
||||||
|
bucketRef = new CategoryBucket
|
||||||
{
|
{
|
||||||
Key = bucketKey,
|
Key = bucketKey,
|
||||||
Category = new CategoryInfo
|
Category = new CategoryInfo
|
||||||
@@ -206,33 +234,44 @@ public static class CategoryBucketManager
|
|||||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||||
Used = true,
|
Used = true,
|
||||||
};
|
};
|
||||||
bucketsByKey. Add(bucketKey, bucket);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bucket.Used = true;
|
bucketRef!.Used = true;
|
||||||
bucket.Category.Name = $"[AT] {filterName}";
|
bucketRef.Category.Name = $"[AT] {filterName}";
|
||||||
|
}
|
||||||
|
|
||||||
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var itemKvp in itemInfoByKey)
|
foreach (var itemKvp in itemInfoByKey)
|
||||||
{
|
{
|
||||||
ulong itemKey = itemKvp.Key;
|
ulong itemKey = itemKvp.Key;
|
||||||
ItemInfo item = itemKvp.Value;
|
|
||||||
|
|
||||||
if (claimedKeys.Contains(itemKey))
|
if (claimedKeys.Contains(itemKey))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (itemIds.ContainsKey(item.Item.ItemId))
|
ItemInfo item = itemKvp.Value;
|
||||||
|
|
||||||
|
if (!itemToFilters.TryGetValue(item.Item.ItemId, out var filterKeys))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (filterKeys.Count > 0 && filterKeyToIndex.TryGetValue(filterKeys[0], out int filterIndex))
|
||||||
|
{
|
||||||
|
uint bucketKey = MakeAllaganFilterKey(filterIndex);
|
||||||
|
if (bucketsByKey.TryGetValue(bucketKey, out var bucket))
|
||||||
{
|
{
|
||||||
bucket.Items.Add(item);
|
bucket.Items.Add(item);
|
||||||
claimedKeys.Add(itemKey);
|
claimedKeys.Add(itemKey);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (bucket.Items.Count == 0)
|
index = 0;
|
||||||
|
foreach (var _ in filters)
|
||||||
|
{
|
||||||
|
uint bucketKey = MakeAllaganFilterKey(index++);
|
||||||
|
if (bucketsByKey.TryGetValue(bucketKey, out var bucket) && bucket.Items.Count == 0)
|
||||||
bucket.Used = false;
|
bucket.Used = false;
|
||||||
|
|
||||||
index++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -250,9 +289,11 @@ public static class CategoryBucketManager
|
|||||||
|
|
||||||
uint bucketKey = MakeBisBuddyKey();
|
uint bucketKey = MakeBisBuddyKey();
|
||||||
|
|
||||||
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
|
ref var bucketRef = ref CollectionsMarshal.GetValueRefOrAddDefault(bucketsByKey, bucketKey, out bool exists);
|
||||||
|
|
||||||
|
if (!exists)
|
||||||
{
|
{
|
||||||
bucket = new CategoryBucket
|
bucketRef = new CategoryBucket
|
||||||
{
|
{
|
||||||
Key = bucketKey,
|
Key = bucketKey,
|
||||||
Category = new CategoryInfo
|
Category = new CategoryInfo
|
||||||
@@ -265,21 +306,22 @@ public static class CategoryBucketManager
|
|||||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||||
Used = true,
|
Used = true,
|
||||||
};
|
};
|
||||||
bucketsByKey.Add(bucketKey, bucket);
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
bucket.Used = true;
|
bucketRef!.Used = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var bucket = bucketRef!;
|
||||||
|
|
||||||
foreach (var itemKvp in itemInfoByKey)
|
foreach (var itemKvp in itemInfoByKey)
|
||||||
{
|
{
|
||||||
ulong itemKey = itemKvp.Key;
|
ulong itemKey = itemKvp.Key;
|
||||||
ItemInfo item = itemKvp.Value;
|
|
||||||
|
|
||||||
if (claimedKeys.Contains(itemKey))
|
if (claimedKeys.Contains(itemKey))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
ItemInfo item = itemKvp.Value;
|
||||||
|
|
||||||
if (bisItems.ContainsKey(item.Item.ItemId))
|
if (bisItems.ContainsKey(item.Item.ItemId))
|
||||||
{
|
{
|
||||||
bucket.Items.Add(item);
|
bucket.Items.Add(item);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
@@ -7,51 +8,20 @@ namespace AetherBags.Inventory.Categories;
|
|||||||
|
|
||||||
internal static class UserCategoryMatcher
|
internal static class UserCategoryMatcher
|
||||||
{
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool Matches(ItemInfo item, UserCategoryDefinition userCategory)
|
public static bool Matches(ItemInfo item, UserCategoryDefinition userCategory)
|
||||||
{
|
{
|
||||||
var rules = userCategory.Rules;
|
var rules = userCategory.Rules;
|
||||||
|
|
||||||
bool hasIdentificationFilters = rules.AllowedItemIds.Count > 0 || rules.AllowedItemNamePatterns.Count > 0;
|
if (!MatchesToggle(rules.Untradable, item.IsUntradable)) return false;
|
||||||
|
if (!MatchesToggle(rules.Unique, item.IsUnique)) return false;
|
||||||
if (hasIdentificationFilters)
|
if (!MatchesToggle(rules.Collectable, item.IsCollectable)) return false;
|
||||||
{
|
if (!MatchesToggle(rules.Dyeable, item.IsDyeable)) return false;
|
||||||
bool matchesAnyIdentification = false;
|
if (!MatchesToggle(rules.HighQuality, item.IsHq)) return false;
|
||||||
|
if (!MatchesToggle(rules.Repairable, item.IsRepairable)) return false;
|
||||||
if (rules.AllowedItemIds.Count > 0 && rules.AllowedItemIds.Contains(item.Item.ItemId))
|
if (!MatchesToggle(rules.Desynthesizable, item.IsDesynthesizable)) return false;
|
||||||
{
|
if (!MatchesToggle(rules.Glamourable, item.IsGlamourable)) return false;
|
||||||
matchesAnyIdentification = true;
|
if (!MatchesToggle(rules.FullySpiritbonded, item.IsSpiritbonded)) return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchesAnyIdentification && rules.AllowedItemNamePatterns.Count > 0)
|
|
||||||
{
|
|
||||||
for (int i = 0; i < rules.AllowedItemNamePatterns.Count; i++)
|
|
||||||
{
|
|
||||||
string pattern = rules.AllowedItemNamePatterns[i];
|
|
||||||
if (string.IsNullOrWhiteSpace(pattern))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
var regex = RegexCache.GetOrCreate(pattern);
|
|
||||||
if (regex != null && regex.IsMatch(item.Name))
|
|
||||||
{
|
|
||||||
matchesAnyIdentification = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!matchesAnyIdentification)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rules.AllowedUiCategoryIds.Count > 0)
|
|
||||||
{
|
|
||||||
uint uiCategoryId = item.UiCategory.RowId;
|
|
||||||
if (!rules.AllowedUiCategoryIds.Contains(uiCategoryId))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rules.AllowedRarities.Count > 0 && !rules.AllowedRarities.Contains(item.Rarity))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
if (rules.Level.Enabled && !InRange(item.Level, rules.Level.Min, rules.Level.Max))
|
if (rules.Level.Enabled && !InRange(item.Level, rules.Level.Min, rules.Level.Max))
|
||||||
return false;
|
return false;
|
||||||
@@ -62,19 +32,40 @@ internal static class UserCategoryMatcher
|
|||||||
if (rules.VendorPrice.Enabled && !InRange(item.VendorPrice, rules.VendorPrice.Min, rules.VendorPrice.Max))
|
if (rules.VendorPrice.Enabled && !InRange(item.VendorPrice, rules.VendorPrice.Min, rules.VendorPrice.Max))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (!MatchesToggle(rules.Untradable, item.IsUntradable)) return false;
|
if (rules.AllowedRarities.Count > 0 && !rules.AllowedRarities.Contains(item.Rarity))
|
||||||
if (!MatchesToggle(rules.Unique, item.IsUnique)) return false;
|
return false;
|
||||||
if (!MatchesToggle(rules.Collectable, item.IsCollectable)) return false;
|
|
||||||
if (!MatchesToggle(rules.Dyeable, item.IsDyeable)) return false;
|
if (rules.AllowedUiCategoryIds.Count > 0 && !rules.AllowedUiCategoryIds.Contains(item.UiCategory.RowId))
|
||||||
if (!MatchesToggle(rules.Repairable, item.IsRepairable)) return false;
|
return false;
|
||||||
if (!MatchesToggle(rules.HighQuality, item.IsHq)) return false;
|
|
||||||
if (!MatchesToggle(rules.Desynthesizable, item.IsDesynthesizable)) return false;
|
bool hasIdentificationFilters = rules.AllowedItemIds.Count > 0 || rules.AllowedItemNamePatterns.Count > 0;
|
||||||
if (!MatchesToggle(rules.Glamourable, item.IsGlamourable)) return false;
|
|
||||||
if (!MatchesToggle(rules.FullySpiritbonded, item.IsSpiritbonded)) return false;
|
if (hasIdentificationFilters)
|
||||||
|
{
|
||||||
|
if (rules.AllowedItemIds.Count > 0 && rules.AllowedItemIds.Contains(item.Item.ItemId))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (rules.AllowedItemNamePatterns.Count > 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < rules.AllowedItemNamePatterns.Count; i++)
|
||||||
|
{
|
||||||
|
string pattern = rules.AllowedItemNamePatterns[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(pattern))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var regex = RegexCache.GetOrCreate(pattern);
|
||||||
|
if (regex != null && regex.IsMatch(item.Name))
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool InRange<T>(T value, T min, T max) where T : struct, IComparable<T>
|
private static bool InRange<T>(T value, T min, T max) where T : struct, IComparable<T>
|
||||||
=> value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
|
=> value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
|
||||||
|
|
||||||
@@ -112,12 +103,13 @@ internal static class UserCategoryMatcher
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static bool MatchesToggle(StateFilter filter, bool itemHasProperty)
|
private static bool MatchesToggle(StateFilter filter, bool itemHasProperty)
|
||||||
=> filter.ToggleState switch
|
|
||||||
{
|
{
|
||||||
ToggleFilterState.Ignored => true,
|
var state = filter.ToggleState;
|
||||||
ToggleFilterState.Allow => itemHasProperty,
|
if (state == ToggleFilterState.Ignored) return true;
|
||||||
ToggleFilterState.Disallow => !itemHasProperty,
|
if (state == ToggleFilterState.Allow) return itemHasProperty;
|
||||||
_ => true
|
if (state == ToggleFilterState.Disallow) return !itemHasProperty;
|
||||||
};
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace AetherBags.Inventory.Context;
|
namespace AetherBags.Inventory.Context;
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ public enum HighlightSource
|
|||||||
Search,
|
Search,
|
||||||
AllaganTools,
|
AllaganTools,
|
||||||
BiSBuddy,
|
BiSBuddy,
|
||||||
|
Relationship,
|
||||||
}
|
}
|
||||||
|
|
||||||
public record HighlightEntry(uint ItemId, Vector3 Color);
|
public record HighlightEntry(uint ItemId, Vector3 Color);
|
||||||
@@ -40,6 +42,7 @@ public static class HighlightState
|
|||||||
_version++;
|
_version++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static bool IsInActiveFilters(uint itemId)
|
public static bool IsInActiveFilters(uint itemId)
|
||||||
{
|
{
|
||||||
if (Filters.Count == 0) return true;
|
if (Filters.Count == 0) return true;
|
||||||
@@ -48,6 +51,7 @@ public static class HighlightState
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static HighlightEntry? GetHighlightEntry(uint itemId)
|
public static HighlightEntry? GetHighlightEntry(uint itemId)
|
||||||
{
|
{
|
||||||
EnsureCacheValid();
|
EnsureCacheValid();
|
||||||
@@ -88,6 +92,7 @@ public static class HighlightState
|
|||||||
_version++;
|
_version++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public static Vector3? GetLabelColor(uint itemId)
|
public static Vector3? GetLabelColor(uint itemId)
|
||||||
=> GetHighlightEntry(itemId)?.Color;
|
=> GetHighlightEntry(itemId)?.Color;
|
||||||
|
|
||||||
@@ -168,4 +173,16 @@ public static class HighlightState
|
|||||||
PerItemLabels.Remove(source);
|
PerItemLabels.Remove(source);
|
||||||
InvalidateCache();
|
InvalidateCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SetRelationshipHighlight(HashSet<uint>? relatedItemIds, Vector3? color)
|
||||||
|
{
|
||||||
|
if (relatedItemIds == null || relatedItemIds.Count == 0)
|
||||||
|
{
|
||||||
|
ClearLabel(HighlightSource.Relationship);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var highlightColor = color ?? new Vector3(0.3f, 0.6f, 0.9f);
|
||||||
|
SetLabel(HighlightSource.Relationship, relatedItemIds, highlightColor);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using AetherBags.Helpers;
|
using AetherBags.Helpers;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using Lumina.Excel;
|
using Lumina.Excel;
|
||||||
using Lumina.Excel.Sheets;
|
using Lumina.Excel.Sheets;
|
||||||
@@ -30,6 +32,7 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
|||||||
private int _cachedHighlightVersion = -1;
|
private int _cachedHighlightVersion = -1;
|
||||||
private float _cachedVisualAlpha;
|
private float _cachedVisualAlpha;
|
||||||
private Vector3 _cachedHighlightColor;
|
private Vector3 _cachedHighlightColor;
|
||||||
|
private bool _cachedIsRelationshipHighlighted;
|
||||||
|
|
||||||
private ref readonly Item Row
|
private ref readonly Item Row
|
||||||
{
|
{
|
||||||
@@ -117,6 +120,15 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool IsRelationshipHighlighted
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
EnsureVisualStateCached();
|
||||||
|
return _cachedIsRelationshipHighlighted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void EnsureVisualStateCached()
|
private void EnsureVisualStateCached()
|
||||||
{
|
{
|
||||||
int currentVersion = HighlightState.Version;
|
int currentVersion = HighlightState.Version;
|
||||||
@@ -127,6 +139,10 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
|||||||
_cachedHighlightColor = System.Config.Categories.BisBuddyEnabled
|
_cachedHighlightColor = System.Config.Categories.BisBuddyEnabled
|
||||||
? HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero
|
? HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero
|
||||||
: Vector3.Zero;
|
: Vector3.Zero;
|
||||||
|
|
||||||
|
var entry = HighlightState.GetHighlightEntry(Item.ItemId);
|
||||||
|
_cachedIsRelationshipHighlighted = entry != null;
|
||||||
|
|
||||||
_cachedHighlightVersion = currentVersion;
|
_cachedHighlightVersion = currentVersion;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +176,7 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
|||||||
|
|
||||||
public bool IsMainInventory => InventoryPage >= 0;
|
public bool IsMainInventory => InventoryPage >= 0;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool IsRegexMatch(string searchTerms)
|
public bool IsRegexMatch(string searchTerms)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(searchTerms))
|
if (string.IsNullOrEmpty(searchTerms))
|
||||||
@@ -171,22 +188,23 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
|||||||
|
|
||||||
if (re.IsMatch(Name)) return true;
|
if (re.IsMatch(Name)) return true;
|
||||||
|
|
||||||
if (re.IsMatch(Description)) return true;
|
|
||||||
|
|
||||||
if (re.IsMatch(LevelString)) return true;
|
if (re.IsMatch(LevelString)) return true;
|
||||||
if (re.IsMatch(ItemLevelString)) return true;
|
if (re.IsMatch(ItemLevelString)) return true;
|
||||||
|
|
||||||
|
if (ExternalCategoryManager.MatchesSearchTag(Item.ItemId, searchTerms)) return true;
|
||||||
|
|
||||||
|
if (re.IsMatch(Description)) return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool IsRegexMatch(Regex re)
|
public bool IsRegexMatch(Regex re)
|
||||||
{
|
{
|
||||||
if (re.IsMatch(Name)) return true;
|
if (re.IsMatch(Name)) return true;
|
||||||
if (re.IsMatch(Description)) return true;
|
|
||||||
|
|
||||||
if (re.IsMatch(LevelString)) return true;
|
if (re.IsMatch(LevelString)) return true;
|
||||||
if (re.IsMatch(ItemLevelString)) return true;
|
if (re.IsMatch(ItemLevelString)) return true;
|
||||||
|
if (re.IsMatch(Description)) return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ using AetherBags.Inventory.Categories;
|
|||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
using AetherBags.Inventory.Scanning;
|
using AetherBags.Inventory.Scanning;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
|
|
||||||
namespace AetherBags.Inventory.State;
|
namespace AetherBags.Inventory.State;
|
||||||
@@ -87,6 +88,24 @@ public abstract class InventoryStateBase
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool useUnified = config.General.UseUnifiedExternalCategories;
|
||||||
|
|
||||||
|
if (useUnified)
|
||||||
|
{
|
||||||
|
ExternalCategoryManager.BucketItems(ItemInfoByKey, BucketsByKey, ClaimedKeys);
|
||||||
|
|
||||||
|
if (allaganCategoriesEnabled && config.Categories.AllaganToolsFilterMode == PluginFilterMode.Highlight)
|
||||||
|
UpdateAllaganHighlight(HighlightState.SelectedAllaganToolsFilterKey);
|
||||||
|
else
|
||||||
|
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
||||||
|
|
||||||
|
if (bisCategoriesEnabled && config.Categories.BisBuddyMode == PluginFilterMode.Highlight)
|
||||||
|
UpdateBisBuddyHighlight(HighlightState.SelectedBisBuddyFilterKey);
|
||||||
|
else
|
||||||
|
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (allaganCategoriesEnabled)
|
if (allaganCategoriesEnabled)
|
||||||
{
|
{
|
||||||
if (config.Categories.AllaganToolsFilterMode == PluginFilterMode.Categorize)
|
if (config.Categories.AllaganToolsFilterMode == PluginFilterMode.Categorize)
|
||||||
@@ -120,6 +139,7 @@ public abstract class InventoryStateBase
|
|||||||
{
|
{
|
||||||
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gameCategoriesEnabled)
|
if (gameCategoriesEnabled)
|
||||||
{
|
{
|
||||||
@@ -205,6 +225,9 @@ public abstract class InventoryStateBase
|
|||||||
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
||||||
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
||||||
|
|
||||||
|
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(List<uint> currencyIds)
|
||||||
|
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
||||||
|
|
||||||
public static void InvalidateCurrencyCaches()
|
public static void InvalidateCurrencyCaches()
|
||||||
=> CurrencyState.InvalidateCaches();
|
=> CurrencyState.InvalidateCaches();
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ public class InventoryMonitor : IDisposable
|
|||||||
|
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, saddle, OnPreFinalize);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, saddle, OnPreFinalize);
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
||||||
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, bags, OnInventoryPreFinalize);
|
||||||
|
|
||||||
// PreRefresh Handlers
|
// PreRefresh Handlers
|
||||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
|
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
|
||||||
@@ -65,6 +66,11 @@ public class InventoryMonitor : IDisposable
|
|||||||
OpenInventories(args.AddonName);
|
OpenInventories(args.AddonName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnInventoryPreFinalize(AddonEvent type, AddonArgs args)
|
||||||
|
{
|
||||||
|
System.AddonInventoryWindow.Close();
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void OpenInventories(string name)
|
private unsafe void OpenInventories(string name)
|
||||||
{
|
{
|
||||||
GeneralSettings config = System.Config.General;
|
GeneralSettings config = System.Config.General;
|
||||||
@@ -168,22 +174,27 @@ public class InventoryMonitor : IDisposable
|
|||||||
|
|
||||||
if (atkValues.Length < 7) return;
|
if (atkValues.Length < 7) return;
|
||||||
|
|
||||||
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
|
|
||||||
AtkValue* value5 = (AtkValue*)atkValues[5].Address;
|
AtkValue* value5 = (AtkValue*)atkValues[5].Address;
|
||||||
AtkValue* value6 = (AtkValue*)atkValues[6].Address;
|
AtkValue* value6 = (AtkValue*)atkValues[6].Address;
|
||||||
|
|
||||||
if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString)
|
if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
int openTitleId = value1->Int;
|
|
||||||
ReadOnlySeString title = value5->String.AsReadOnlySeString();
|
ReadOnlySeString title = value5->String.AsReadOnlySeString();
|
||||||
ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString();
|
ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString();
|
||||||
|
|
||||||
System.AddonInventoryWindow.SetNotification(new InventoryNotificationInfo(title, upperTitle));
|
System.AddonInventoryWindow.SetNotification(new InventoryNotificationInfo(title, upperTitle));
|
||||||
|
|
||||||
if (config.HideGameInventory) refreshArgs.AtkValueCount = 0;
|
if (config.HideGameInventory)
|
||||||
|
{
|
||||||
|
refreshArgs.AtkValueCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (config.OpenWithGameInventory)
|
if (config.OpenWithGameInventory)
|
||||||
{
|
{
|
||||||
|
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
|
||||||
|
int openTitleId = value1->Int;
|
||||||
|
|
||||||
if (openTitleId == 0)
|
if (openTitleId == 0)
|
||||||
{
|
{
|
||||||
System.AddonInventoryWindow.Toggle();
|
System.AddonInventoryWindow.Toggle();
|
||||||
@@ -234,6 +245,6 @@ public class InventoryMonitor : IDisposable
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
Services.GameInventory.InventoryChangedRaw -= OnInventoryChangedRaw;
|
||||||
Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate);
|
Services.AddonLifecycle.UnregisterListener(OnPostSetup, OnPreFinalize, OnInventoryUpdate, OnSaddleBagUpdate, OnRetainerInventoryUpdate, OnInventoryPreFinalize, InventoryPreRefreshHandler);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -25,6 +25,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
private bool _isEnabled;
|
private bool _isEnabled;
|
||||||
private long _batchStartTick;
|
private long _batchStartTick;
|
||||||
private bool _hasPendingRemoval;
|
private bool _hasPendingRemoval;
|
||||||
|
private int _nextIndex;
|
||||||
|
|
||||||
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
||||||
|
|
||||||
@@ -32,8 +33,6 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
|
|
||||||
public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval;
|
public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval;
|
||||||
|
|
||||||
private int GetNextIndex() => _lootedItems.Count > 0 ? _lootedItems.Max(x => x.Index) + 1 : 0;
|
|
||||||
|
|
||||||
public void Enable()
|
public void Enable()
|
||||||
{
|
{
|
||||||
if (_isEnabled) return;
|
if (_isEnabled) return;
|
||||||
@@ -43,6 +42,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
_pendingChanges.Clear();
|
_pendingChanges.Clear();
|
||||||
_batchStartTick = 0;
|
_batchStartTick = 0;
|
||||||
_hasPendingRemoval = false;
|
_hasPendingRemoval = false;
|
||||||
|
_nextIndex = 0;
|
||||||
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
||||||
Services.Framework.Update += OnFrameworkUpdate;
|
Services.Framework.Update += OnFrameworkUpdate;
|
||||||
}
|
}
|
||||||
@@ -58,12 +58,14 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
_pendingChanges.Clear();
|
_pendingChanges.Clear();
|
||||||
_batchStartTick = 0;
|
_batchStartTick = 0;
|
||||||
_hasPendingRemoval = false;
|
_hasPendingRemoval = false;
|
||||||
|
_nextIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
_lootedItems.Clear();
|
_lootedItems.Clear();
|
||||||
_hasPendingRemoval = true;
|
_hasPendingRemoval = true;
|
||||||
|
_nextIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveByIndex(int index)
|
public void RemoveByIndex(int index)
|
||||||
@@ -100,9 +102,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
|
|
||||||
foreach (var ((itemId, isHq), (item, delta)) in _pendingChanges)
|
foreach (var ((itemId, isHq), (item, delta)) in _pendingChanges)
|
||||||
{
|
{
|
||||||
int existingIndex = _lootedItems.FindIndex(x =>
|
int existingIndex = FindExistingItemIndex(itemId, isHq);
|
||||||
x.Item.ItemId == itemId &&
|
|
||||||
x.Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) == isHq);
|
|
||||||
|
|
||||||
if (existingIndex >= 0)
|
if (existingIndex >= 0)
|
||||||
{
|
{
|
||||||
@@ -116,13 +116,27 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
}
|
}
|
||||||
else if (delta > 0)
|
else if (delta > 0)
|
||||||
{
|
{
|
||||||
_lootedItems.Add(new LootedItemInfo(GetNextIndex(), item, delta));
|
_lootedItems.Add(new LootedItemInfo(_nextIndex++, item, delta));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_pendingChanges.Clear();
|
_pendingChanges.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int FindExistingItemIndex(uint itemId, bool isHq)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _lootedItems.Count; i++)
|
||||||
|
{
|
||||||
|
var info = _lootedItems[i];
|
||||||
|
if (info.Item.ItemId == itemId &&
|
||||||
|
info.Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) == isHq)
|
||||||
|
{
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
private void OnInventoryChangedRaw(IReadOnlyCollection<InventoryEventArgs> events)
|
private void OnInventoryChangedRaw(IReadOnlyCollection<InventoryEventArgs> events)
|
||||||
{
|
{
|
||||||
if (!_isEnabled || !Services.ClientState.IsLoggedIn) return;
|
if (!_isEnabled || !Services.ClientState.IsLoggedIn) return;
|
||||||
@@ -156,7 +170,12 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
|||||||
|
|
||||||
if (_pendingChanges.TryGetValue(key, out var existing))
|
if (_pendingChanges.TryGetValue(key, out var existing))
|
||||||
{
|
{
|
||||||
_pendingChanges[key] = (existing.Item, existing.Quantity + changeAmount);
|
InventoryItem itemStruct = existing.Item;
|
||||||
|
if (changeAmount > 0 && itemStruct.ItemId == 0)
|
||||||
|
{
|
||||||
|
itemStruct = *(InventoryItem*)eventData.Item.Address;
|
||||||
|
}
|
||||||
|
_pendingChanges[key] = (itemStruct, existing.Quantity + changeAmount);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
|||||||
OnClick = isChecked =>
|
OnClick = isChecked =>
|
||||||
{
|
{
|
||||||
config.CategoriesEnabled = isChecked;
|
config.CategoriesEnabled = isChecked;
|
||||||
|
System.IPC?.RefreshExternalSources();
|
||||||
RefreshInventory();
|
RefreshInventory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -92,8 +93,9 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
|||||||
{
|
{
|
||||||
config.BisBuddyMode = selected;
|
config.BisBuddyMode = selected;
|
||||||
if (selected == PluginFilterMode.Categorize)
|
if (selected == PluginFilterMode.Categorize)
|
||||||
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
||||||
|
|
||||||
|
System.IPC?.RefreshExternalSources();
|
||||||
RefreshInventory();
|
RefreshInventory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -110,6 +112,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
|||||||
config.BisBuddyEnabled = isChecked;
|
config.BisBuddyEnabled = isChecked;
|
||||||
if (bbModeDropdown != null) bbModeDropdown.IsEnabled = isChecked;
|
if (bbModeDropdown != null) bbModeDropdown.IsEnabled = isChecked;
|
||||||
if (isChecked) System.IPC.BisBuddy?.RefreshItems();
|
if (isChecked) System.IPC.BisBuddy?.RefreshItems();
|
||||||
|
System.IPC?.RefreshExternalSources();
|
||||||
RefreshInventory();
|
RefreshInventory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -134,6 +137,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
|||||||
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
System.IPC?.RefreshExternalSources();
|
||||||
RefreshInventory();
|
RefreshInventory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -153,6 +157,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
|||||||
config.AllaganToolsCategoriesEnabled = isChecked;
|
config.AllaganToolsCategoriesEnabled = isChecked;
|
||||||
if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked;
|
if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked;
|
||||||
if (isChecked) System.IPC?.AllaganTools?.RefreshFilters();
|
if (isChecked) System.IPC?.AllaganTools?.RefreshFilters();
|
||||||
|
System.IPC?.RefreshExternalSources();
|
||||||
RefreshInventory();
|
RefreshInventory();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ public sealed class CategoryScrollingAreaNode : ScrollingListNode
|
|||||||
|
|
||||||
AddNode(new CategoryGeneralConfigurationNode());
|
AddNode(new CategoryGeneralConfigurationNode());
|
||||||
|
|
||||||
|
AddNode(new ExperimentalConfigurationNode());
|
||||||
|
|
||||||
var categoryConfigurationButtonNode = new TextButtonNode
|
var categoryConfigurationButtonNode = new TextButtonNode
|
||||||
{
|
{
|
||||||
Size = new Vector2(300, 28),
|
Size = new Vector2(300, 28),
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
using AetherBags.Configuration;
|
||||||
|
using AetherBags.Inventory;
|
||||||
|
using KamiToolKit.Nodes;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Configuration.Category;
|
||||||
|
|
||||||
|
internal class ExperimentalConfigurationNode : TabbedVerticalListNode
|
||||||
|
{
|
||||||
|
public ExperimentalConfigurationNode()
|
||||||
|
{
|
||||||
|
GeneralSettings config = System.Config.General;
|
||||||
|
|
||||||
|
var titleNode = new CategoryTextNode
|
||||||
|
{
|
||||||
|
Height = 18,
|
||||||
|
String = "Experimental",
|
||||||
|
};
|
||||||
|
AddNode(titleNode);
|
||||||
|
|
||||||
|
AddTab(1);
|
||||||
|
|
||||||
|
var externalCategoryCheckbox = new CheckboxNode
|
||||||
|
{
|
||||||
|
Height = 18,
|
||||||
|
IsVisible = true,
|
||||||
|
String = "External Category Support",
|
||||||
|
IsChecked = config.UseUnifiedExternalCategories,
|
||||||
|
TextTooltip = "EXPERIMENTAL - Use at your own risk. This feature is not fully tested.\n\n" +
|
||||||
|
"Enables enhanced integration with external plugins like " +
|
||||||
|
"Allagan Tools and BisBuddy.\n\n" +
|
||||||
|
"Features:\n" +
|
||||||
|
"- Search by plugin tags (e.g. search 'bis' to find BiS items)\n" +
|
||||||
|
"- Relationship highlighting: hover an item to see related items\n" +
|
||||||
|
" (same gear set, upgrades, crafting materials)\n" +
|
||||||
|
"- Item badges showing plugin status icons\n" +
|
||||||
|
"- Custom borders and visual effects (glow, pulse)\n" +
|
||||||
|
"- Additional right-click menu options from plugins\n" +
|
||||||
|
"- Extra tooltip information from plugins\n\n" +
|
||||||
|
"When disabled, external plugins still provide categories and " +
|
||||||
|
"basic highlighting, but without these enhanced features.",
|
||||||
|
OnClick = isChecked =>
|
||||||
|
{
|
||||||
|
config.UseUnifiedExternalCategories = isChecked;
|
||||||
|
System.IPC?.UpdateUnifiedCategorySupport(isChecked);
|
||||||
|
InventoryOrchestrator.RefreshAll(updateMaps: true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
AddNode(externalCategoryCheckbox);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,8 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
private readonly TextNode _categoryNameTextNode;
|
private readonly TextNode _categoryNameTextNode;
|
||||||
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
private readonly HybridDirectionalFlexNode<DragDropNode> _itemGridNode;
|
||||||
|
|
||||||
private const float FallbackItemSize = 46;
|
private const float ExpectedItemWidth = 42;
|
||||||
|
private const float ExpectedItemHeight = 46;
|
||||||
private const float HeaderHeight = 16;
|
private const float HeaderHeight = 16;
|
||||||
private const float MinWidth = 40;
|
private const float MinWidth = 40;
|
||||||
|
|
||||||
@@ -41,12 +42,16 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
private int _lastItemCount;
|
private int _lastItemCount;
|
||||||
private ulong _lastItemsHash;
|
private ulong _lastItemsHash;
|
||||||
private int _lastItemsPerLine;
|
private int _lastItemsPerLine;
|
||||||
private float? _lastMaxWidth;
|
private bool _itemsNeedPopulation;
|
||||||
|
|
||||||
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
||||||
|
|
||||||
|
public bool NeedsItemPopulation => _itemsNeedPopulation;
|
||||||
public Action? OnRefreshRequested { get; set; }
|
public Action? OnRefreshRequested { get; set; }
|
||||||
public Action? OnDragEnd { get; set; }
|
public Action? OnDragEnd { get; set; }
|
||||||
|
|
||||||
|
public SharedNodePool<InventoryDragDropNode>? SharedItemPool { get; set; }
|
||||||
|
|
||||||
public InventoryCategoryNode()
|
public InventoryCategoryNode()
|
||||||
{
|
{
|
||||||
_categoryNameTextNode = new TextNode
|
_categoryNameTextNode = new TextNode
|
||||||
@@ -86,11 +91,10 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
set => SetCategoryData(value, _itemGridNode.ItemsPerLine);
|
set => SetCategoryData(value, _itemGridNode.ItemsPerLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void SetCategoryData(CategorizedInventory data, int itemsPerLine)
|
public void SetCategoryData(CategorizedInventory data, int itemsPerLine, bool deferItemCreation = false)
|
||||||
{
|
{
|
||||||
bool categoryChanged = data.Key != _lastCategoryKey;
|
bool categoryChanged = data.Key != _lastCategoryKey;
|
||||||
bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine;
|
bool itemsPerLineChanged = itemsPerLine != _lastItemsPerLine;
|
||||||
bool maxWidthChanged = _maxWidth != _lastMaxWidth;
|
|
||||||
|
|
||||||
ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items));
|
ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items));
|
||||||
bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash;
|
bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash;
|
||||||
@@ -99,7 +103,6 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
_lastItemCount = data.Items.Count;
|
_lastItemCount = data.Items.Count;
|
||||||
_lastItemsHash = itemsHash;
|
_lastItemsHash = itemsHash;
|
||||||
_lastItemsPerLine = itemsPerLine;
|
_lastItemsPerLine = itemsPerLine;
|
||||||
_lastMaxWidth = _maxWidth;
|
|
||||||
|
|
||||||
_categorizedInventory = data;
|
_categorizedInventory = data;
|
||||||
|
|
||||||
@@ -112,24 +115,45 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
_categoryNameTextNode.TextTooltip = data.Category.Description;
|
_categoryNameTextNode.TextTooltip = data.Category.Description;
|
||||||
|
|
||||||
if (itemsChanged || categoryChanged)
|
if (itemsChanged || categoryChanged)
|
||||||
|
{
|
||||||
|
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||||
|
|
||||||
|
if (deferItemCreation)
|
||||||
|
{
|
||||||
|
_itemsNeedPopulation = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
using (_itemGridNode.DeferRecalculateLayout())
|
using (_itemGridNode.DeferRecalculateLayout())
|
||||||
{
|
{
|
||||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
|
||||||
UpdateItemGrid();
|
UpdateItemGrid();
|
||||||
}
|
}
|
||||||
|
_itemsNeedPopulation = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (itemsPerLineChanged)
|
else if (itemsPerLineChanged)
|
||||||
{
|
{
|
||||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (categoryChanged || itemsChanged || itemsPerLineChanged || maxWidthChanged)
|
if (categoryChanged || itemsChanged || itemsPerLineChanged)
|
||||||
{
|
{
|
||||||
RecalculateSize();
|
RecalculateSize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void PopulateItems()
|
||||||
|
{
|
||||||
|
if (!_itemsNeedPopulation)
|
||||||
|
return;
|
||||||
|
|
||||||
|
using (_itemGridNode.DeferRecalculateLayout())
|
||||||
|
{
|
||||||
|
UpdateItemGrid();
|
||||||
|
}
|
||||||
|
_itemsNeedPopulation = false;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items)
|
private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items)
|
||||||
{
|
{
|
||||||
@@ -164,7 +188,7 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public float? MaxWidth
|
public override float? MaxWidth
|
||||||
{
|
{
|
||||||
get => _maxWidth;
|
get => _maxWidth;
|
||||||
set => _maxWidth = value;
|
set => _maxWidth = value;
|
||||||
@@ -234,12 +258,12 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RecalculateSize()
|
public override void RecalculateSize()
|
||||||
{
|
{
|
||||||
int itemCount = CategorizedInventory.Items.Count;
|
int itemCount = CategorizedInventory.Items.Count;
|
||||||
|
|
||||||
float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize;
|
float cellW = ExpectedItemWidth;
|
||||||
float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize;
|
float cellH = ExpectedItemHeight;
|
||||||
float hPad = _itemGridNode.HorizontalPadding;
|
float hPad = _itemGridNode.HorizontalPadding;
|
||||||
float vPad = _itemGridNode.VerticalPadding;
|
float vPad = _itemGridNode.VerticalPadding;
|
||||||
|
|
||||||
@@ -298,31 +322,55 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
getKeyFromData: item => item.Key,
|
getKeyFromData: item => item.Key,
|
||||||
getKeyFromNode: node => node.ItemInfo?.Key ?? 0,
|
getKeyFromNode: node => node.ItemInfo?.Key ?? 0,
|
||||||
updateNode: UpdateInventoryDragDropNode,
|
updateNode: UpdateInventoryDragDropNode,
|
||||||
createNodeMethod: CreateInventoryDragDropNode);
|
createNodeMethod: CreateInventoryDragDropNode,
|
||||||
|
resetNodeForReuse: ResetDragDropNodeForReuse,
|
||||||
|
externalPool: SharedItemPool);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateInventoryDragDropNode(InventoryDragDropNode node, ItemInfo data)
|
private void UpdateInventoryDragDropNode(InventoryDragDropNode node, ItemInfo data)
|
||||||
{
|
|
||||||
if (node.ItemInfo?.Key == data.Key)
|
|
||||||
{
|
{
|
||||||
node.ItemInfo = data;
|
node.ItemInfo = data;
|
||||||
node.Alpha = data.VisualAlpha;
|
ApplyItemDataToNode(node, data);
|
||||||
node.AddColor = data.HighlightOverlayColor;
|
|
||||||
node.IsDraggable = !data.IsSlotBlocked;
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ResetDragDropNodeForReuse(InventoryDragDropNode node)
|
||||||
|
{
|
||||||
|
node.ResetForReuse();
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
||||||
|
{
|
||||||
|
var node = new InventoryDragDropNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(42, 46),
|
||||||
|
IsVisible = true,
|
||||||
|
AcceptedType = DragDropType.Item,
|
||||||
|
IsClickable = true,
|
||||||
|
OnDiscard = OnNodeDiscard,
|
||||||
|
OnEnd = _ => OnDragEnd?.Invoke(),
|
||||||
|
OnPayloadAccepted = OnNodePayloadAccepted,
|
||||||
|
OnRollOver = OnNodeRollOver,
|
||||||
|
OnRollOut = OnNodeRollOut,
|
||||||
|
ItemInfo = data
|
||||||
|
};
|
||||||
|
|
||||||
|
ApplyItemDataToNode(node, data);
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyItemDataToNode(InventoryDragDropNode node, ItemInfo data)
|
||||||
|
{
|
||||||
InventoryItem item = data.Item;
|
InventoryItem item = data.Item;
|
||||||
InventoryMappedLocation visualLocation = data.VisualLocation;
|
InventoryMappedLocation visualLocation = data.VisualLocation;
|
||||||
|
|
||||||
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
||||||
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
||||||
|
|
||||||
node.ItemInfo = data;
|
|
||||||
node.IconId = item.IconId;
|
node.IconId = item.IconId;
|
||||||
node.Alpha = data.VisualAlpha;
|
node.Alpha = data.VisualAlpha;
|
||||||
node.AddColor = data.HighlightOverlayColor;
|
node.AddColor = data.HighlightOverlayColor;
|
||||||
node.IsDraggable = !data.IsSlotBlocked;
|
node.IsDraggable = !data.IsSlotBlocked;
|
||||||
|
node.IconNode.IconExtras.AntsNode.IsVisible = data.IsRelationshipHighlighted;
|
||||||
node.Payload = new DragDropPayload
|
node.Payload = new DragDropPayload
|
||||||
{
|
{
|
||||||
Type = DragDropType.Item,
|
Type = DragDropType.Item,
|
||||||
@@ -332,52 +380,31 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe InventoryDragDropNode CreateInventoryDragDropNode(ItemInfo data)
|
private void OnNodeDiscard(DragDropNode n)
|
||||||
{
|
{
|
||||||
InventoryItem item = data.Item;
|
if (n is not InventoryDragDropNode node) return;
|
||||||
InventoryMappedLocation visualLocation = data.VisualLocation;
|
OnDiscard(n, node.ItemInfo);
|
||||||
|
}
|
||||||
|
|
||||||
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
private void OnNodePayloadAccepted(DragDropNode n, DragDropPayload acceptedPayload)
|
||||||
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
{
|
||||||
|
if (n is not InventoryDragDropNode node) return;
|
||||||
|
OnPayloadAccepted(n, acceptedPayload, node.ItemInfo);
|
||||||
|
}
|
||||||
|
|
||||||
DragDropPayload nodePayload = new DragDropPayload
|
private unsafe void OnNodeRollOver(DragDropNode n)
|
||||||
{
|
|
||||||
// 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.VisualAlpha,
|
|
||||||
AddColor = data.HighlightOverlayColor,
|
|
||||||
IsDraggable = !data.IsSlotBlocked,
|
|
||||||
IsVisible = true,
|
|
||||||
IconId = item.IconId,
|
|
||||||
AcceptedType = DragDropType.Item,
|
|
||||||
Payload = nodePayload,
|
|
||||||
IsClickable = true,
|
|
||||||
OnDiscard = node => OnDiscard(node, data),
|
|
||||||
OnEnd = _ => OnDragEnd?.Invoke(),
|
|
||||||
OnPayloadAccepted = (node, acceptedPayload) => OnPayloadAccepted(node, acceptedPayload, data),
|
|
||||||
OnRollOver = node =>
|
|
||||||
{
|
{
|
||||||
|
if (n is not InventoryDragDropNode node) return;
|
||||||
BeginHeaderHover();
|
BeginHeaderHover();
|
||||||
node.ShowInventoryItemTooltip(item.Container, item.Slot);
|
var item = node.ItemInfo.Item;
|
||||||
},
|
n.ShowInventoryItemTooltip(item.Container, item.Slot);
|
||||||
OnRollOut = node =>
|
}
|
||||||
|
|
||||||
|
private unsafe void OnNodeRollOut(DragDropNode n)
|
||||||
{
|
{
|
||||||
EndHeaderHover();
|
EndHeaderHover();
|
||||||
|
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(n)->Id;
|
||||||
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
|
|
||||||
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
|
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
|
||||||
},
|
|
||||||
ItemInfo = data
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RefreshNodeVisuals()
|
public void RefreshNodeVisuals()
|
||||||
@@ -392,6 +419,7 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
float newAlpha = info.VisualAlpha;
|
float newAlpha = info.VisualAlpha;
|
||||||
Vector3 newColor = info.HighlightOverlayColor;
|
Vector3 newColor = info.HighlightOverlayColor;
|
||||||
bool newDraggable = !info.IsSlotBlocked;
|
bool newDraggable = !info.IsSlotBlocked;
|
||||||
|
bool newAntsVisible = info.IsRelationshipHighlighted;
|
||||||
|
|
||||||
if (!NearlyEqual(itemNode.Alpha, newAlpha))
|
if (!NearlyEqual(itemNode.Alpha, newAlpha))
|
||||||
itemNode.Alpha = newAlpha;
|
itemNode.Alpha = newAlpha;
|
||||||
@@ -401,6 +429,9 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
if (itemNode.IsDraggable != newDraggable)
|
if (itemNode.IsDraggable != newDraggable)
|
||||||
itemNode.IsDraggable = newDraggable;
|
itemNode.IsDraggable = newDraggable;
|
||||||
|
|
||||||
|
if (itemNode.IconNode.IconExtras.AntsNode.IsVisible != newAntsVisible)
|
||||||
|
itemNode.IconNode.IconExtras.AntsNode.IsVisible = newAntsVisible;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,4 +485,30 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
|||||||
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
|
Services.Logger.Error(ex, "[OnPayload] Error handling payload acceptance");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ResetForReuse()
|
||||||
|
{
|
||||||
|
_lastCategoryKey = 0;
|
||||||
|
_lastItemCount = 0;
|
||||||
|
_lastItemsHash = 0;
|
||||||
|
_lastItemsPerLine = 0;
|
||||||
|
_itemsNeedPopulation = false;
|
||||||
|
|
||||||
|
_hoverRefs = 0;
|
||||||
|
_headerSuppressed = false;
|
||||||
|
_headerExpanded = false;
|
||||||
|
_fullHeaderText = string.Empty;
|
||||||
|
|
||||||
|
_fixedWidth = null;
|
||||||
|
_maxWidth = null;
|
||||||
|
|
||||||
|
_categoryNameTextNode.String = string.Empty;
|
||||||
|
_categoryNameTextNode.TextTooltip = string.Empty;
|
||||||
|
_categoryNameTextNode.IsVisible = true;
|
||||||
|
|
||||||
|
using (_itemGridNode.DeferRecalculateLayout())
|
||||||
|
{
|
||||||
|
_itemGridNode.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,4 +17,8 @@ public abstract class InventoryCategoryNodeBase : SimpleComponentNode
|
|||||||
/// Whether this category should be pinned in the layout.
|
/// Whether this category should be pinned in the layout.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual bool IsPinnedInConfig => false;
|
public virtual bool IsPinnedInConfig => false;
|
||||||
|
|
||||||
|
public abstract float? MaxWidth { get; set; }
|
||||||
|
|
||||||
|
public abstract void RecalculateSize();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
using AetherBags.Addons;
|
||||||
|
using AetherBags.Inventory.Context;
|
||||||
using AetherBags.Inventory.Items;
|
using AetherBags.Inventory.Items;
|
||||||
|
using AetherBags.IPC.ExternalCategorySystem;
|
||||||
using Dalamud.Game.ClientState.Keys;
|
using Dalamud.Game.ClientState.Keys;
|
||||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||||
using KamiToolKit.Classes;
|
using KamiToolKit.Classes;
|
||||||
using KamiToolKit.Nodes;
|
using KamiToolKit.Nodes;
|
||||||
|
using KamiToolKit.Timelines;
|
||||||
|
|
||||||
namespace AetherBags.Nodes.Inventory;
|
namespace AetherBags.Nodes.Inventory;
|
||||||
|
|
||||||
public class InventoryDragDropNode : DragDropNode
|
public class InventoryDragDropNode : DragDropNode
|
||||||
{
|
{
|
||||||
private readonly TextNode _quantityTextNode;
|
private readonly TextNode _quantityTextNode;
|
||||||
|
private IconNode? _badgeNode;
|
||||||
|
private ImageNode? _borderNode;
|
||||||
|
private ItemDecoration? _currentDecoration;
|
||||||
|
|
||||||
public unsafe InventoryDragDropNode()
|
public unsafe InventoryDragDropNode()
|
||||||
{
|
{
|
||||||
_quantityTextNode = new TextNode {
|
_quantityTextNode = new TextNode {
|
||||||
@@ -26,6 +34,8 @@ public class InventoryDragDropNode : DragDropNode
|
|||||||
_quantityTextNode.AttachNode(this);
|
_quantityTextNode.AttachNode(this);
|
||||||
CollisionNode.AddEvent(AtkEventType.MouseDown, OnItemMouseDown);
|
CollisionNode.AddEvent(AtkEventType.MouseDown, OnItemMouseDown);
|
||||||
CollisionNode.AddEvent(AtkEventType.MouseClick, OnItemClicked);
|
CollisionNode.AddEvent(AtkEventType.MouseClick, OnItemClicked);
|
||||||
|
CollisionNode.AddEvent(AtkEventType.MouseOver, OnItemHover);
|
||||||
|
CollisionNode.AddEvent(AtkEventType.MouseOut, OnItemUnhover);
|
||||||
}
|
}
|
||||||
|
|
||||||
public required ItemInfo ItemInfo
|
public required ItemInfo ItemInfo
|
||||||
@@ -35,9 +45,164 @@ public class InventoryDragDropNode : DragDropNode
|
|||||||
{
|
{
|
||||||
field = value;
|
field = value;
|
||||||
_quantityTextNode.String = value.ItemCount.ToString();
|
_quantityTextNode.String = value.ItemCount.ToString();
|
||||||
|
ApplyDecoration(ExternalCategoryManager.GetDecoration(value.Item.ItemId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ApplyDecoration(ItemDecoration? decoration)
|
||||||
|
{
|
||||||
|
if (_currentDecoration.Equals(decoration)) return;
|
||||||
|
_currentDecoration = decoration;
|
||||||
|
|
||||||
|
if (decoration == null)
|
||||||
|
{
|
||||||
|
ClearDecoration();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoration.Value.Badge.HasValue)
|
||||||
|
{
|
||||||
|
ApplyBadge(decoration.Value.Badge.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearBadge();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoration.Value.Border != BorderStyle.None)
|
||||||
|
{
|
||||||
|
ApplyBorder(decoration.Value.Border);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ClearBorder();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyBadge(BadgeInfo badge)
|
||||||
|
{
|
||||||
|
if (_badgeNode == null)
|
||||||
|
{
|
||||||
|
_badgeNode = new IconNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(16, 16),
|
||||||
|
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled,
|
||||||
|
};
|
||||||
|
_badgeNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_badgeNode.IconId = badge.IconId;
|
||||||
|
_badgeNode.IsVisible = true;
|
||||||
|
|
||||||
|
if (badge.TintColor.HasValue)
|
||||||
|
{
|
||||||
|
_badgeNode.AddColor = new Vector3(badge.TintColor.Value.X, badge.TintColor.Value.Y, badge.TintColor.Value.Z);
|
||||||
|
}
|
||||||
|
|
||||||
|
_badgeNode.Position = badge.Position switch
|
||||||
|
{
|
||||||
|
BadgePosition.TopLeft => new Vector2(0, 0),
|
||||||
|
BadgePosition.TopRight => new Vector2(26, 0),
|
||||||
|
BadgePosition.BottomLeft => new Vector2(0, 30),
|
||||||
|
BadgePosition.BottomRight => new Vector2(26, 30),
|
||||||
|
_ => new Vector2(26, 0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearBadge()
|
||||||
|
{
|
||||||
|
if (_badgeNode != null)
|
||||||
|
{
|
||||||
|
_badgeNode.IsVisible = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BorderStyle _currentBorderStyle = BorderStyle.None;
|
||||||
|
|
||||||
|
private void ApplyBorder(BorderStyle style)
|
||||||
|
{
|
||||||
|
if (_borderNode == null)
|
||||||
|
{
|
||||||
|
_borderNode = new SimpleImageNode
|
||||||
|
{
|
||||||
|
Size = new Vector2(42, 46),
|
||||||
|
Position = new Vector2(0, 0),
|
||||||
|
NodeFlags = NodeFlags.Visible | NodeFlags.Enabled,
|
||||||
|
TexturePath = "ui/uld/IconA_Frame.tex",
|
||||||
|
TextureCoordinates = new Vector2(0, 0),
|
||||||
|
TextureSize = new Vector2(48, 48),
|
||||||
|
};
|
||||||
|
_borderNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
_borderNode.IsVisible = true;
|
||||||
|
|
||||||
|
if (_currentBorderStyle != style)
|
||||||
|
{
|
||||||
|
_currentBorderStyle = style;
|
||||||
|
BuildBorderTimeline(style);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style == BorderStyle.Pulse)
|
||||||
|
{
|
||||||
|
_borderNode.Timeline?.PlayAnimation(1);
|
||||||
|
}
|
||||||
|
else if (style == BorderStyle.Glow)
|
||||||
|
{
|
||||||
|
_borderNode.Timeline?.PlayAnimation(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildBorderTimeline(BorderStyle style)
|
||||||
|
{
|
||||||
|
if (_borderNode == null) return;
|
||||||
|
|
||||||
|
switch (style)
|
||||||
|
{
|
||||||
|
case BorderStyle.Solid:
|
||||||
|
_borderNode.AddColor = new Vector3(1.0f, 1.0f, 1.0f);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BorderStyle.Glow:
|
||||||
|
_borderNode.AddTimeline(new TimelineBuilder()
|
||||||
|
.BeginFrameSet(10, 50)
|
||||||
|
.AddLabel(10, 1, AtkTimelineJumpBehavior.LoopForever, 10)
|
||||||
|
.AddFrame(10, addColor: new Vector3(0.6f, 0.8f, 1.0f), alpha: 255)
|
||||||
|
.AddFrame(30, addColor: new Vector3(0.9f, 1.0f, 1.2f), alpha: 255)
|
||||||
|
.AddFrame(50, addColor: new Vector3(0.6f, 0.8f, 1.0f), alpha: 255)
|
||||||
|
.EndFrameSet()
|
||||||
|
.Build());
|
||||||
|
break;
|
||||||
|
|
||||||
|
case BorderStyle.Pulse:
|
||||||
|
_borderNode.AddTimeline(new TimelineBuilder()
|
||||||
|
.BeginFrameSet(1, 40)
|
||||||
|
.AddLabel(1, 1, AtkTimelineJumpBehavior.LoopForever, 1)
|
||||||
|
.AddFrame(1, addColor: new Vector3(1.0f, 0.6f, 0.0f), alpha: 180)
|
||||||
|
.AddFrame(20, addColor: new Vector3(1.2f, 0.9f, 0.3f), alpha: 255)
|
||||||
|
.AddFrame(40, addColor: new Vector3(1.0f, 0.6f, 0.0f), alpha: 180)
|
||||||
|
.EndFrameSet()
|
||||||
|
.Build());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearBorder()
|
||||||
|
{
|
||||||
|
if (_borderNode != null)
|
||||||
|
{
|
||||||
|
_borderNode.Timeline?.StopAnimation();
|
||||||
|
_borderNode.IsVisible = false;
|
||||||
|
_currentBorderStyle = BorderStyle.None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ClearDecoration()
|
||||||
|
{
|
||||||
|
ClearBadge();
|
||||||
|
ClearBorder();
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void OnItemMouseDown(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
private unsafe void OnItemMouseDown(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData) {
|
||||||
InventoryItem item = ItemInfo.Item;
|
InventoryItem item = ItemInfo.Item;
|
||||||
if (Services.KeyState[VirtualKey.SHIFT] && atkEventData->IsLeftClick && System.Config.General.LinkItemEnabled)
|
if (Services.KeyState[VirtualKey.SHIFT] && atkEventData->IsLeftClick && System.Config.General.LinkItemEnabled)
|
||||||
@@ -48,6 +213,11 @@ public class InventoryDragDropNode : DragDropNode
|
|||||||
|
|
||||||
if (!atkEventData->IsRightClick) return;
|
if (!atkEventData->IsRightClick) return;
|
||||||
|
|
||||||
|
if (Services.KeyState[VirtualKey.CONTROL] && ItemContextMenuHandler.TryShowExternalMenu(ItemInfo))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
AgentInventoryContext* context = AgentInventoryContext.Instance();
|
AgentInventoryContext* context = AgentInventoryContext.Instance();
|
||||||
context->OpenForItemSlot(item.Container, item.Slot, 0, context->AddonId);
|
context->OpenForItemSlot(item.Container, item.Slot, 0, context->AddonId);
|
||||||
}
|
}
|
||||||
@@ -57,6 +227,56 @@ public class InventoryDragDropNode : DragDropNode
|
|||||||
if (Services.KeyState[VirtualKey.SHIFT] && System.Config.General.LinkItemEnabled) return;
|
if (Services.KeyState[VirtualKey.SHIFT] && System.Config.General.LinkItemEnabled) return;
|
||||||
InventoryItem item = ItemInfo.Item;
|
InventoryItem item = ItemInfo.Item;
|
||||||
if (!atkEventData->IsLeftClick) return;
|
if (!atkEventData->IsLeftClick) return;
|
||||||
|
|
||||||
|
System.AetherBagsAPI?.API.RaiseItemClicked(item.ItemId);
|
||||||
item.UseItem();
|
item.UseItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe void OnItemHover(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
uint itemId = ItemInfo.Item.ItemId;
|
||||||
|
System.AetherBagsAPI?.API.RaiseItemHovered(itemId);
|
||||||
|
|
||||||
|
if (System.Config.General.UseUnifiedExternalCategories)
|
||||||
|
{
|
||||||
|
var relatedItems = ExternalCategoryManager.GetRelatedItemIds(itemId, RelationshipType.SameSet);
|
||||||
|
if (relatedItems != null && relatedItems.Count > 0)
|
||||||
|
{
|
||||||
|
var relationships = ExternalCategoryManager.GetItemRelationships(itemId);
|
||||||
|
Vector3? highlightColor = null;
|
||||||
|
if (relationships != null)
|
||||||
|
{
|
||||||
|
foreach (var rel in relationships)
|
||||||
|
{
|
||||||
|
if (rel.Type == RelationshipType.SameSet && rel.HighlightColor.HasValue)
|
||||||
|
{
|
||||||
|
highlightColor = rel.HighlightColor;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HighlightState.SetRelationshipHighlight(relatedItems, highlightColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe void OnItemUnhover(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
System.AetherBagsAPI?.API.RaiseItemUnhovered(ItemInfo.Item.ItemId);
|
||||||
|
|
||||||
|
if (System.Config.General.UseUnifiedExternalCategories)
|
||||||
|
{
|
||||||
|
HighlightState.SetRelationshipHighlight(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetForReuse()
|
||||||
|
{
|
||||||
|
ClearDecoration();
|
||||||
|
_quantityTextNode.String = string.Empty;
|
||||||
|
Alpha = 1.0f;
|
||||||
|
AddColor = Vector3.Zero;
|
||||||
|
IsDraggable = true;
|
||||||
|
IconNode.IconExtras.AntsNode.IsVisible = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -48,8 +48,7 @@ public sealed class InventoryFooterNode : SimpleComponentNode
|
|||||||
|
|
||||||
if (!config.Enabled) return;
|
if (!config.Enabled) return;
|
||||||
|
|
||||||
//IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
|
IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList(config.DisplayedCurrencies);
|
||||||
IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList(config.DisplayedCurrencies.ToArray());
|
|
||||||
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
|
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
|
||||||
dataList: currencyInfoList,
|
dataList: currencyInfoList,
|
||||||
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
|||||||
private readonly TextNode _quantityTextNode;
|
private readonly TextNode _quantityTextNode;
|
||||||
|
|
||||||
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
||||||
|
public Action<LootedItemDisplayNode>? OnRollOver { get; set; }
|
||||||
|
public Action<LootedItemDisplayNode>? OnRollOut { get; set; }
|
||||||
|
|
||||||
public LootedItemDisplayNode()
|
public LootedItemDisplayNode()
|
||||||
{
|
{
|
||||||
@@ -28,9 +30,13 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
|||||||
Position = new Vector2(0, 0),
|
Position = new Vector2(0, 0),
|
||||||
Size = new Vector2(42, 46),
|
Size = new Vector2(42, 46),
|
||||||
};
|
};
|
||||||
_iconNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
_iconNode.CollisionNode.NodeFlags = 0;
|
||||||
_iconNode.AttachNode(this);
|
_iconNode.AttachNode(this);
|
||||||
|
|
||||||
|
CollisionNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||||
|
CollisionNode.AddEvent(AtkEventType.MouseOver, OnMouseOver);
|
||||||
|
CollisionNode.AddEvent(AtkEventType.MouseOut, OnMouseOut);
|
||||||
|
|
||||||
_quantityTextNode = new TextNode
|
_quantityTextNode = new TextNode
|
||||||
{
|
{
|
||||||
Size = new Vector2(40.0f, 12.0f),
|
Size = new Vector2(40.0f, 12.0f),
|
||||||
@@ -80,4 +86,21 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
|||||||
if (!atkEventData->IsLeftClick) return;
|
if (!atkEventData->IsLeftClick) return;
|
||||||
OnDismiss?.Invoke(this);
|
OnDismiss?.Invoke(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnMouseOver(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
OnRollOver?.Invoke(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMouseOut(AtkEventListener* thisPtr, AtkEventType eventType, int eventParam, AtkEvent* atkEvent, AtkEventData* atkEventData)
|
||||||
|
{
|
||||||
|
OnRollOut?.Invoke(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ResetForReuse()
|
||||||
|
{
|
||||||
|
LootedItem = null;
|
||||||
|
_iconNode.IsVisible = false;
|
||||||
|
_quantityTextNode.String = string.Empty;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
private int _lastItemCount;
|
private int _lastItemCount;
|
||||||
private long _lastItemsHash;
|
private long _lastItemsHash;
|
||||||
|
|
||||||
|
private float? _maxWidth;
|
||||||
|
|
||||||
private int _hoverRefs;
|
private int _hoverRefs;
|
||||||
private bool _headerExpanded;
|
private bool _headerExpanded;
|
||||||
private float _baseHeaderWidth = 96f;
|
private float _baseHeaderWidth = 96f;
|
||||||
@@ -54,6 +56,12 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
public bool HasItems => _lootedItems.Count > 0;
|
public bool HasItems => _lootedItems.Count > 0;
|
||||||
|
|
||||||
|
public override float? MaxWidth
|
||||||
|
{
|
||||||
|
get => _maxWidth;
|
||||||
|
set => _maxWidth = value;
|
||||||
|
}
|
||||||
|
|
||||||
public LootedItemsCategoryNode()
|
public LootedItemsCategoryNode()
|
||||||
{
|
{
|
||||||
_headerTextNode = new TextNode
|
_headerTextNode = new TextNode
|
||||||
@@ -183,6 +191,8 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
Vector2 drawSize = _headerTextNode.GetTextDrawSize();
|
Vector2 drawSize = _headerTextNode.GetTextDrawSize();
|
||||||
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
|
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
|
||||||
_headerTextNode.Size = _headerTextNode.Size with { X = expandedWidth };
|
_headerTextNode.Size = _headerTextNode.Size with { X = expandedWidth };
|
||||||
|
|
||||||
|
_clearButton.Position = new Vector2(expandedWidth + 4f, (HeaderHeight - ClearButtonSize) / 2);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -193,6 +203,9 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
|
|
||||||
flags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
flags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||||
_headerTextNode.TextFlags = flags;
|
_headerTextNode.TextFlags = flags;
|
||||||
|
|
||||||
|
float nodeWidth = Size.X;
|
||||||
|
_clearButton.Position = new Vector2(nodeWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -203,7 +216,8 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
getKeyFromData: item => item.Index,
|
getKeyFromData: item => item.Index,
|
||||||
getKeyFromNode: node => node.LootedItem?.Index ?? -1,
|
getKeyFromNode: node => node.LootedItem?.Index ?? -1,
|
||||||
updateNode: UpdateLootedItemNode,
|
updateNode: UpdateLootedItemNode,
|
||||||
createNodeMethod: CreateLootedItemNode);
|
createNodeMethod: CreateLootedItemNode,
|
||||||
|
resetNodeForReuse: ResetLootedItemNodeForReuse);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void UpdateLootedItemNode(LootedItemDisplayNode node, LootedItemInfo data)
|
private static void UpdateLootedItemNode(LootedItemDisplayNode node, LootedItemInfo data)
|
||||||
@@ -211,11 +225,18 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
node.LootedItem = data;
|
node.LootedItem = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ResetLootedItemNodeForReuse(LootedItemDisplayNode node)
|
||||||
|
{
|
||||||
|
node.ResetForReuse();
|
||||||
|
}
|
||||||
|
|
||||||
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||||
{
|
{
|
||||||
return new LootedItemDisplayNode
|
return new LootedItemDisplayNode
|
||||||
{
|
{
|
||||||
OnDismiss = OnItemDismissed,
|
OnDismiss = OnItemDismissed,
|
||||||
|
OnRollOver = _ => BeginHeaderHover(),
|
||||||
|
OnRollOut = _ => EndHeaderHover(),
|
||||||
LootedItem = lootedItem,
|
LootedItem = lootedItem,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -227,13 +248,19 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
OnDismissItem?.Invoke(index);
|
OnDismissItem?.Invoke(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RecalculateSize()
|
public override void RecalculateSize()
|
||||||
{
|
{
|
||||||
int itemCount = _lootedItems.Count;
|
int itemCount = _lootedItems.Count;
|
||||||
|
|
||||||
|
const float cellW = 42f;
|
||||||
|
const float cellH = 46f;
|
||||||
|
|
||||||
|
float hPad = _itemGridNode.HorizontalPadding;
|
||||||
|
float vPad = _itemGridNode.VerticalPadding;
|
||||||
|
|
||||||
if (itemCount == 0)
|
if (itemCount == 0)
|
||||||
{
|
{
|
||||||
float width = MinWidth;
|
float width = _maxWidth.HasValue ? Math.Min(MinWidth, _maxWidth.Value) : MinWidth;
|
||||||
Size = new Vector2(width, HeaderHeight);
|
Size = new Vector2(width, HeaderHeight);
|
||||||
_baseHeaderWidth = width - ClearButtonSize - 4;
|
_baseHeaderWidth = width - ClearButtonSize - 4;
|
||||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||||
@@ -246,20 +273,36 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
|
int itemsPerLine = Math.Max(1, _itemGridNode.ItemsPerLine);
|
||||||
|
|
||||||
|
float minUsableWidth = cellW;
|
||||||
|
if (_maxWidth.HasValue && _maxWidth.Value >= minUsableWidth)
|
||||||
|
{
|
||||||
|
int maxColumns = (int)MathF.Floor((_maxWidth.Value + hPad) / (cellW + hPad));
|
||||||
|
maxColumns = Math.Max(1, maxColumns);
|
||||||
|
|
||||||
|
float widthNeeded = maxColumns * cellW + (maxColumns - 1) * hPad;
|
||||||
|
if (widthNeeded > _maxWidth.Value && maxColumns > 1)
|
||||||
|
maxColumns--;
|
||||||
|
|
||||||
|
itemsPerLine = Math.Min(itemsPerLine, maxColumns);
|
||||||
|
}
|
||||||
|
|
||||||
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
|
int rows = (itemCount + itemsPerLine - 1) / itemsPerLine;
|
||||||
int actualColumns = Math.Min(itemCount, itemsPerLine);
|
int actualColumns = Math.Min(itemCount, itemsPerLine);
|
||||||
|
|
||||||
const float cellW = 42f;
|
|
||||||
const float cellH = 46f;
|
|
||||||
|
|
||||||
float hPad = _itemGridNode.HorizontalPadding;
|
|
||||||
float vPad = _itemGridNode.VerticalPadding;
|
|
||||||
|
|
||||||
float calculatedWidth = Math.Max(MinWidth, actualColumns * cellW + (actualColumns - 1) * hPad);
|
float calculatedWidth = Math.Max(MinWidth, actualColumns * cellW + (actualColumns - 1) * hPad);
|
||||||
|
|
||||||
|
if (_maxWidth.HasValue && _maxWidth.Value >= minUsableWidth)
|
||||||
|
calculatedWidth = Math.Min(calculatedWidth, _maxWidth.Value);
|
||||||
|
|
||||||
float gridHeight = rows * cellH + (rows - 1) * vPad;
|
float gridHeight = rows * cellH + (rows - 1) * vPad;
|
||||||
float totalHeight = HeaderHeight + gridHeight;
|
float totalHeight = HeaderHeight + gridHeight;
|
||||||
|
|
||||||
Size = new Vector2(calculatedWidth, totalHeight);
|
Size = new Vector2(calculatedWidth, totalHeight);
|
||||||
|
|
||||||
|
if (_itemGridNode.ItemsPerLine != itemsPerLine)
|
||||||
|
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||||
|
|
||||||
_baseHeaderWidth = calculatedWidth - ClearButtonSize - 4;
|
_baseHeaderWidth = calculatedWidth - ClearButtonSize - 4;
|
||||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||||
_clearButton.Position = new Vector2(calculatedWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
_clearButton.Position = new Vector2(calculatedWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||||
|
|||||||
@@ -15,20 +15,75 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
private int _deferRecalcDepth;
|
private int _deferRecalcDepth;
|
||||||
private bool _pendingRecalc;
|
private bool _pendingRecalc;
|
||||||
|
|
||||||
/// <summary>
|
private readonly Dictionary<Type, Stack<NodeBase>> _nodePoolByType = new();
|
||||||
/// Hide and detach a node from the UI tree without disposing it.
|
private const int MaxPoolSizePerType = 64;
|
||||||
/// Disposal happens later when KamiToolKit cleans up detached nodes.
|
|
||||||
/// </summary>
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
protected static void SafeDetachNode(NodeBase node)
|
private TU? TryRentFromPool<TU>(SharedNodePool<TU>? externalPool) where TU : NodeBase
|
||||||
|
{
|
||||||
|
if (externalPool != null)
|
||||||
|
{
|
||||||
|
return externalPool.TryRent();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_nodePoolByType.TryGetValue(typeof(TU), out var pool) && pool.Count > 0)
|
||||||
|
{
|
||||||
|
var node = (TU)pool.Pop();
|
||||||
|
node.IsVisible = true;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool TryReturnToPool<TU>(TU node, SharedNodePool<TU>? externalPool, Action<TU>? resetAction) where TU : NodeBase
|
||||||
|
{
|
||||||
|
if (externalPool != null)
|
||||||
|
{
|
||||||
|
resetAction?.Invoke(node);
|
||||||
|
return externalPool.TryReturn(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = typeof(TU);
|
||||||
|
if (!_nodePoolByType.TryGetValue(type, out var pool))
|
||||||
|
{
|
||||||
|
pool = new Stack<NodeBase>(16);
|
||||||
|
_nodePoolByType[type] = pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool.Count >= MaxPoolSizePerType)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
resetAction?.Invoke(node);
|
||||||
|
node.IsVisible = false;
|
||||||
|
node.DetachNode();
|
||||||
|
pool.Push(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DisposePool()
|
||||||
|
{
|
||||||
|
foreach (var pool in _nodePoolByType.Values)
|
||||||
|
{
|
||||||
|
while (pool.Count > 0)
|
||||||
|
{
|
||||||
|
var node = pool.Pop();
|
||||||
|
SafeDisposeNode(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_nodePoolByType.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
protected static void SafeDisposeNode(NodeBase node)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
node.IsVisible = false;
|
node.Dispose();
|
||||||
node.DetachNode();
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Services.Logger.Error(ex, $"[SafeDetachNode] Error detaching {node.GetType().Name}");
|
Services.Logger.Error(ex, $"[SafeDisposeNode] Error disposing {node.GetType().Name}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,7 +192,7 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
if (!NodeList.Contains(node)) return;
|
if (!NodeList.Contains(node)) return;
|
||||||
|
|
||||||
NodeList.Remove(node);
|
NodeList.Remove(node);
|
||||||
SafeDetachNode(node);
|
SafeDisposeNode(node);
|
||||||
|
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
@@ -161,13 +216,16 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
{
|
{
|
||||||
var node = NodeList[i];
|
var node = NodeList[i];
|
||||||
NodeList.RemoveAt(i);
|
NodeList.RemoveAt(i);
|
||||||
SafeDetachNode(node);
|
SafeDisposeNode(node);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_suppressRecalculateLayout = false;
|
_suppressRecalculateLayout = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DisposePool();
|
||||||
|
|
||||||
RecalculateLayout();
|
RecalculateLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,13 +306,14 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
_dataKeysScratch = set;
|
_dataKeysScratch = set;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool SyncWithListDataByKey<T, TU, TKey>(
|
public bool SyncWithListDataByKey<T, TU, TKey>(
|
||||||
IReadOnlyList<T> dataList,
|
IReadOnlyList<T> dataList,
|
||||||
Func<T, TKey> getKeyFromData,
|
Func<T, TKey> getKeyFromData,
|
||||||
Func<TU, TKey> getKeyFromNode,
|
Func<TU, TKey> getKeyFromNode,
|
||||||
Action<TU, T> updateNode,
|
Action<TU, T> updateNode,
|
||||||
CreateNewNode<T, TU> createNodeMethod,
|
CreateNewNode<T, TU> createNodeMethod,
|
||||||
|
Action<TU>? resetNodeForReuse = null,
|
||||||
|
SharedNodePool<TU>? externalPool = null,
|
||||||
IEqualityComparer<TKey>? keyComparer = null) where TU : NodeBase where TKey : notnull
|
IEqualityComparer<TKey>? keyComparer = null) where TU : NodeBase where TKey : notnull
|
||||||
{
|
{
|
||||||
keyComparer ??= EqualityComparer<TKey>.Default;
|
keyComparer ??= EqualityComparer<TKey>.Default;
|
||||||
@@ -295,9 +354,13 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
{
|
{
|
||||||
for (int i = 0; i < toRemove.Count; i++)
|
for (int i = 0; i < toRemove.Count; i++)
|
||||||
{
|
{
|
||||||
var node = toRemove[i];
|
var node = (TU)toRemove[i];
|
||||||
NodeList.Remove(node);
|
NodeList.Remove(node);
|
||||||
SafeDetachNode(node);
|
|
||||||
|
if (!TryReturnToPool(node, externalPool, resetNodeForReuse))
|
||||||
|
{
|
||||||
|
SafeDisposeNode(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -345,9 +408,20 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var newNode = createNodeMethod(data);
|
TU newNode;
|
||||||
NodeList.Add(newNode);
|
var pooledNode = TryRentFromPool(externalPool);
|
||||||
|
if (pooledNode != null)
|
||||||
|
{
|
||||||
|
newNode = pooledNode;
|
||||||
newNode.AttachNode(this);
|
newNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newNode = createNodeMethod(data);
|
||||||
|
newNode.AttachNode(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
NodeList.Add(newNode);
|
||||||
updateNode(newNode, data);
|
updateNode(newNode, data);
|
||||||
desired.Add(newNode);
|
desired.Add(newNode);
|
||||||
structureChanged = true;
|
structureChanged = true;
|
||||||
@@ -425,6 +499,15 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
return structureChanged || orderChanged;
|
return structureChanged || orderChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool SyncWithListDataByKey<T, TU, TKey>(
|
||||||
|
IReadOnlyList<T> dataList,
|
||||||
|
Func<T, TKey> getKeyFromData,
|
||||||
|
Func<TU, TKey> getKeyFromNode,
|
||||||
|
Action<TU, T> updateNode,
|
||||||
|
CreateNewNode<T, TU> createNodeMethod,
|
||||||
|
IEqualityComparer<TKey>? keyComparer) where TU : NodeBase where TKey : notnull
|
||||||
|
=> SyncWithListDataByKey(dataList, getKeyFromData, getKeyFromNode, updateNode, createNodeMethod, null, null, keyComparer);
|
||||||
|
|
||||||
public bool SyncWithListData<T, TU>(
|
public bool SyncWithListData<T, TU>(
|
||||||
IEnumerable<T> dataList,
|
IEnumerable<T> dataList,
|
||||||
GetDataFromNode<T?, TU> getDataFromNode,
|
GetDataFromNode<T?, TU> getDataFromNode,
|
||||||
@@ -455,7 +538,7 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
|||||||
if (nodeData is null || !dataSet.Contains(nodeData))
|
if (nodeData is null || !dataSet.Contains(nodeData))
|
||||||
{
|
{
|
||||||
NodeList.Remove(tu);
|
NodeList.Remove(tu);
|
||||||
SafeDetachNode(tu);
|
SafeDisposeNode(tu);
|
||||||
anythingChanged = true;
|
anythingChanged = true;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using KamiToolKit;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
|
public sealed class SharedNodePool<T> where T : NodeBase
|
||||||
|
{
|
||||||
|
private readonly Stack<T> _pool;
|
||||||
|
private readonly int _maxSize;
|
||||||
|
private readonly Func<T>? _factory;
|
||||||
|
private readonly Action<T>? _resetAction;
|
||||||
|
|
||||||
|
public SharedNodePool(int maxSize = 128, Func<T>? factory = null, Action<T>? resetAction = null)
|
||||||
|
{
|
||||||
|
_maxSize = maxSize;
|
||||||
|
_factory = factory;
|
||||||
|
_resetAction = resetAction;
|
||||||
|
_pool = new Stack<T>(Math.Min(maxSize, 64));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Count => _pool.Count;
|
||||||
|
public int MaxSize => _maxSize;
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public T? TryRent()
|
||||||
|
{
|
||||||
|
if (_pool.TryPop(out var node))
|
||||||
|
{
|
||||||
|
node.IsVisible = true;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public T RentOrCreate()
|
||||||
|
{
|
||||||
|
if (_pool.TryPop(out var node))
|
||||||
|
{
|
||||||
|
node.IsVisible = true;
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_factory == null)
|
||||||
|
throw new InvalidOperationException("No factory provided and pool is empty");
|
||||||
|
|
||||||
|
return _factory();
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool TryReturn(T node)
|
||||||
|
{
|
||||||
|
if (_pool.Count >= _maxSize)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
_resetAction?.Invoke(node);
|
||||||
|
node.IsVisible = false;
|
||||||
|
node.DetachNode();
|
||||||
|
_pool.Push(node);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Return(T node)
|
||||||
|
{
|
||||||
|
if (!TryReturn(node))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
node.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Services.Logger.Error(ex, $"[SharedNodePool] Error disposing overflow node {typeof(T).Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
while (_pool.TryPop(out var node))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
node.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Services.Logger.Error(ex, $"[SharedNodePool] Error disposing pooled node {typeof(T).Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Prewarm(int count)
|
||||||
|
{
|
||||||
|
if (_factory == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
count = Math.Min(count, _maxSize - _pool.Count);
|
||||||
|
for (int i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var node = _factory();
|
||||||
|
node.IsVisible = false;
|
||||||
|
_pool.Push(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace AetherBags.Nodes.Layout;
|
||||||
|
|
||||||
|
public sealed class VirtualizationState
|
||||||
|
{
|
||||||
|
private float _scrollPosition;
|
||||||
|
private float _viewportHeight;
|
||||||
|
private float _bufferSize = 100f;
|
||||||
|
|
||||||
|
private readonly List<VisibilityInfo> _itemVisibility = new(capacity: 64);
|
||||||
|
|
||||||
|
public float ScrollPosition
|
||||||
|
{
|
||||||
|
get => _scrollPosition;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (MathF.Abs(_scrollPosition - value) < 0.5f) return;
|
||||||
|
_scrollPosition = value;
|
||||||
|
UpdateVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float ViewportHeight
|
||||||
|
{
|
||||||
|
get => _viewportHeight;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (MathF.Abs(_viewportHeight - value) < 0.5f) return;
|
||||||
|
_viewportHeight = value;
|
||||||
|
UpdateVisibility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float BufferSize
|
||||||
|
{
|
||||||
|
get => _bufferSize;
|
||||||
|
set => _bufferSize = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public event Action? OnVisibilityChanged;
|
||||||
|
|
||||||
|
public void SetItemLayout(int index, float y, float height)
|
||||||
|
{
|
||||||
|
while (_itemVisibility.Count <= index)
|
||||||
|
{
|
||||||
|
_itemVisibility.Add(new VisibilityInfo());
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = _itemVisibility[index];
|
||||||
|
info.Y = y;
|
||||||
|
info.Height = height;
|
||||||
|
_itemVisibility[index] = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearLayout()
|
||||||
|
{
|
||||||
|
_itemVisibility.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetItemCount(int count)
|
||||||
|
{
|
||||||
|
while (_itemVisibility.Count < count)
|
||||||
|
{
|
||||||
|
_itemVisibility.Add(new VisibilityInfo());
|
||||||
|
}
|
||||||
|
if (_itemVisibility.Count > count)
|
||||||
|
{
|
||||||
|
_itemVisibility.RemoveRange(count, _itemVisibility.Count - count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsVisible(int index)
|
||||||
|
{
|
||||||
|
if (index < 0 || index >= _itemVisibility.Count)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return _itemVisibility[index].IsVisible;
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public bool IsInVisibleRange(float y, float height)
|
||||||
|
{
|
||||||
|
float visibleTop = _scrollPosition - _bufferSize;
|
||||||
|
float visibleBottom = _scrollPosition + _viewportHeight + _bufferSize;
|
||||||
|
|
||||||
|
float itemTop = y;
|
||||||
|
float itemBottom = y + height;
|
||||||
|
|
||||||
|
return itemBottom >= visibleTop && itemTop <= visibleBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void UpdateVisibility()
|
||||||
|
{
|
||||||
|
bool anyChanged = false;
|
||||||
|
float visibleTop = _scrollPosition - _bufferSize;
|
||||||
|
float visibleBottom = _scrollPosition + _viewportHeight + _bufferSize;
|
||||||
|
|
||||||
|
for (int i = 0; i < _itemVisibility.Count; i++)
|
||||||
|
{
|
||||||
|
var info = _itemVisibility[i];
|
||||||
|
float itemTop = info.Y;
|
||||||
|
float itemBottom = info.Y + info.Height;
|
||||||
|
|
||||||
|
bool wasVisible = info.IsVisible;
|
||||||
|
bool isVisible = itemBottom >= visibleTop && itemTop <= visibleBottom;
|
||||||
|
|
||||||
|
if (wasVisible != isVisible)
|
||||||
|
{
|
||||||
|
info.IsVisible = isVisible;
|
||||||
|
_itemVisibility[i] = info;
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChanged)
|
||||||
|
{
|
||||||
|
OnVisibilityChanged?.Invoke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GetVisibleRange(out int firstVisible, out int lastVisible)
|
||||||
|
{
|
||||||
|
firstVisible = -1;
|
||||||
|
lastVisible = -1;
|
||||||
|
|
||||||
|
for (int i = 0; i < _itemVisibility.Count; i++)
|
||||||
|
{
|
||||||
|
if (_itemVisibility[i].IsVisible)
|
||||||
|
{
|
||||||
|
if (firstVisible < 0) firstVisible = i;
|
||||||
|
lastVisible = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct VisibilityInfo
|
||||||
|
{
|
||||||
|
public float Y;
|
||||||
|
public float Height;
|
||||||
|
public bool IsVisible;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
private int _lastCompactLookahead;
|
private int _lastCompactLookahead;
|
||||||
|
|
||||||
private int[] _orderScratch = Array.Empty<int>();
|
private int[] _orderScratch = Array.Empty<int>();
|
||||||
|
private bool _forceFullReflow;
|
||||||
|
|
||||||
private T? _hoistedNode;
|
private T? _hoistedNode;
|
||||||
private readonly HashSet<T> _pinned = new(ReferenceEqualityComparer<T>.Instance);
|
private readonly HashSet<T> _pinned = new(ReferenceEqualityComparer<T>.Instance);
|
||||||
@@ -43,6 +44,7 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
private readonly List<NodeBase> _layoutOrder = new(capacity: 256);
|
private readonly List<NodeBase> _layoutOrder = new(capacity: 256);
|
||||||
private readonly List<NodeBase> _pinnedScratch = new(capacity: 64);
|
private readonly List<NodeBase> _pinnedScratch = new(capacity: 64);
|
||||||
private readonly List<NodeBase> _normalScratch = new(capacity: 256);
|
private readonly List<NodeBase> _normalScratch = new(capacity: 256);
|
||||||
|
private readonly HashSet<T> _presentScratch = new(ReferenceEqualityComparer<T>.Instance);
|
||||||
|
|
||||||
public WrappingGridNode()
|
public WrappingGridNode()
|
||||||
{
|
{
|
||||||
@@ -56,6 +58,11 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public bool TryGetRowIndex(NodeBase node, out int rowIndex) => _rowIndex.TryGetValue(node, out rowIndex);
|
public bool TryGetRowIndex(NodeBase node, out int rowIndex) => _rowIndex.TryGetValue(node, out rowIndex);
|
||||||
|
|
||||||
|
public void InvalidateLayout()
|
||||||
|
{
|
||||||
|
_forceFullReflow = true;
|
||||||
|
}
|
||||||
|
|
||||||
public void SetHoistedNode(T? node)
|
public void SetHoistedNode(T? node)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_hoistedNode, node))
|
if (ReferenceEquals(_hoistedNode, node))
|
||||||
@@ -111,10 +118,14 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
_rowIndex.Clear();
|
_rowIndex.Clear();
|
||||||
_requiredHeight = 0f;
|
_requiredHeight = 0f;
|
||||||
_requiredHeightDirty = false;
|
_requiredHeightDirty = false;
|
||||||
|
_forceFullReflow = false;
|
||||||
RememberLayoutParams();
|
RememberLayoutParams();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool forceReflow = _forceFullReflow;
|
||||||
|
_forceFullReflow = false;
|
||||||
|
|
||||||
bool hasSpecials = hoistedCount != 0 || pinnedCount != 0;
|
bool hasSpecials = hoistedCount != 0 || pinnedCount != 0;
|
||||||
bool compactEnabled = System.Config.General.CompactPackingEnabled;
|
bool compactEnabled = System.Config.General.CompactPackingEnabled;
|
||||||
|
|
||||||
@@ -128,7 +139,7 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_rows.Count != 0 && LayoutParamsMatchLast() && NodeSetMatchesExistingLayout(layoutCount))
|
if (!forceReflow && _rows.Count != 0 && LayoutParamsMatchLast() && NodeSetMatchesExistingLayout(layoutCount))
|
||||||
{
|
{
|
||||||
RepositionExistingRows();
|
RepositionExistingRows();
|
||||||
_requiredHeightDirty = true;
|
_requiredHeightDirty = true;
|
||||||
@@ -142,7 +153,8 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_rows.Count != 0 &&
|
if (!forceReflow &&
|
||||||
|
_rows.Count != 0 &&
|
||||||
NodeSetMatchesExistingLayout(layoutCount) &&
|
NodeSetMatchesExistingLayout(layoutCount) &&
|
||||||
TryUpdateLayoutWithoutReflowOrTailReflow(layoutCount, hoistedCount, pinnedCount))
|
TryUpdateLayoutWithoutReflowOrTailReflow(layoutCount, hoistedCount, pinnedCount))
|
||||||
{
|
{
|
||||||
@@ -173,7 +185,8 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
var present = new HashSet<T>(ReferenceEqualityComparer<T>.Instance);
|
_presentScratch.Clear();
|
||||||
|
var present = _presentScratch;
|
||||||
|
|
||||||
bool hoistedPresent = false;
|
bool hoistedPresent = false;
|
||||||
T? hoisted = _hoistedNode;
|
T? hoisted = _hoistedNode;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using AetherBags.Hooks;
|
|||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.Inventory.Context;
|
using AetherBags.Inventory.Context;
|
||||||
using AetherBags.IPC;
|
using AetherBags.IPC;
|
||||||
|
using AetherBags.IPC.AetherBagsAPI;
|
||||||
using AetherBags.Monitoring;
|
using AetherBags.Monitoring;
|
||||||
using Dalamud.Plugin;
|
using Dalamud.Plugin;
|
||||||
using KamiToolKit;
|
using KamiToolKit;
|
||||||
@@ -29,6 +30,8 @@ public class Plugin : IDalamudPlugin
|
|||||||
KamiToolKitLibrary.Initialize(pluginInterface);
|
KamiToolKitLibrary.Initialize(pluginInterface);
|
||||||
|
|
||||||
System.IPC = new IPCService();
|
System.IPC = new IPCService();
|
||||||
|
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||||
|
ItemContextMenuHandler.Initialize();
|
||||||
System.LootedItemsTracker = new LootedItemsTracker();
|
System.LootedItemsTracker = new LootedItemsTracker();
|
||||||
|
|
||||||
System.AddonInventoryWindow = new AddonInventoryWindow
|
System.AddonInventoryWindow = new AddonInventoryWindow
|
||||||
@@ -62,6 +65,8 @@ public class Plugin : IDalamudPlugin
|
|||||||
Services.PluginInterface.UiBuilder.OpenMainUi += System.AddonInventoryWindow.Toggle;
|
Services.PluginInterface.UiBuilder.OpenMainUi += System.AddonInventoryWindow.Toggle;
|
||||||
Services.PluginInterface.UiBuilder.OpenConfigUi += System.AddonConfigurationWindow.Toggle;
|
Services.PluginInterface.UiBuilder.OpenConfigUi += System.AddonConfigurationWindow.Toggle;
|
||||||
|
|
||||||
|
System.AetherBagsAPI = new AetherBagsIPCProvider();
|
||||||
|
|
||||||
_commandHandler = new CommandHandler();
|
_commandHandler = new CommandHandler();
|
||||||
|
|
||||||
Services.ClientState.Login += OnLogin;
|
Services.ClientState.Login += OnLogin;
|
||||||
@@ -78,10 +83,12 @@ public class Plugin : IDalamudPlugin
|
|||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
InventoryAddonContextMenu.Close();
|
InventoryAddonContextMenu.Close();
|
||||||
|
ItemContextMenuHandler.Dispose();
|
||||||
_inventoryHooks.Dispose();
|
_inventoryHooks.Dispose();
|
||||||
inventoryMonitor.Dispose();
|
inventoryMonitor.Dispose();
|
||||||
|
|
||||||
System.LootedItemsTracker.Dispose();
|
System.LootedItemsTracker.Dispose();
|
||||||
|
System.AetherBagsAPI?.Dispose();
|
||||||
System.IPC.Dispose();
|
System.IPC.Dispose();
|
||||||
HighlightState.ClearAll();
|
HighlightState.ClearAll();
|
||||||
|
|
||||||
@@ -97,6 +104,7 @@ public class Plugin : IDalamudPlugin
|
|||||||
private void OnLogin()
|
private void OnLogin()
|
||||||
{
|
{
|
||||||
System.Config = Util.LoadConfigOrDefault();
|
System.Config = Util.LoadConfigOrDefault();
|
||||||
|
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||||
System.LootedItemsTracker.Enable();
|
System.LootedItemsTracker.Enable();
|
||||||
|
|
||||||
System.AddonInventoryWindow.DebugOpen();
|
System.AddonInventoryWindow.DebugOpen();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using AetherBags.Addons;
|
|||||||
using AetherBags.Configuration;
|
using AetherBags.Configuration;
|
||||||
using AetherBags.Inventory;
|
using AetherBags.Inventory;
|
||||||
using AetherBags.IPC;
|
using AetherBags.IPC;
|
||||||
|
using AetherBags.IPC.AetherBagsAPI;
|
||||||
using AetherBags.Monitoring;
|
using AetherBags.Monitoring;
|
||||||
|
|
||||||
namespace AetherBags;
|
namespace AetherBags;
|
||||||
@@ -13,6 +14,7 @@ public static class System
|
|||||||
public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!;
|
public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!;
|
||||||
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
public static AddonConfigurationWindow AddonConfigurationWindow { get; set; } = null!;
|
||||||
public static IPCService IPC { get; set; } = null!;
|
public static IPCService IPC { get; set; } = null!;
|
||||||
|
public static AetherBagsIPCProvider? AetherBagsAPI { get; set; }
|
||||||
public static SystemConfiguration Config { get; set; } = null!;
|
public static SystemConfiguration Config { get; set; } = null!;
|
||||||
public static LootedItemsTracker LootedItemsTracker { get; set; } = null!;
|
public static LootedItemsTracker LootedItemsTracker { get; set; } = null!;
|
||||||
}
|
}
|
||||||
+1
-1
Submodule KamiToolKit updated: 811154c8f8...7720ab0741
Reference in New Issue
Block a user