Merge branch 'dev/pie-lover'
This commit is contained in:
@@ -25,16 +25,20 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
{
|
||||
InitializeBackgroundDropTarget();
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
HorizontalSpacing = CategorySpacing,
|
||||
VerticalSpacing = CategorySpacing,
|
||||
TopPadding = 4.0f,
|
||||
BottomPadding = 4.0f,
|
||||
ContentHeight = 0f,
|
||||
AutoHideScrollBar = true,
|
||||
};
|
||||
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
|
||||
{
|
||||
@@ -127,6 +131,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
CategoriesNode.RemoveNode(_lootedCategoryNode);
|
||||
}
|
||||
CategoriesNode.InvalidateLayout();
|
||||
AutoSizeWindow();
|
||||
}
|
||||
}
|
||||
@@ -134,6 +139,7 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
private void OnDismissLootedItem(int index)
|
||||
{
|
||||
System.LootedItemsTracker.RemoveByIndex(index);
|
||||
System.LootedItemsTracker.FlushPendingChanges();
|
||||
}
|
||||
|
||||
private void OnClearAllLootedItems()
|
||||
@@ -148,6 +154,21 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
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)
|
||||
{
|
||||
Services.Framework.RunOnTick(() =>
|
||||
@@ -168,6 +189,8 @@ public unsafe class AddonInventoryWindow : InventoryAddonBase
|
||||
|
||||
addon->UnsubscribeAtkArrayData(1, (int)NumberArrayType.Inventory);
|
||||
|
||||
_lootedCategoryNode?.Dispose();
|
||||
|
||||
IsSetupComplete = false;
|
||||
base.OnFinalize(addon);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
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;
|
||||
|
||||
private readonly string[] _retainerAddonNames = { "InventoryRetainer", "InventoryRetainerLarge" };
|
||||
@@ -38,16 +38,20 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
HorizontalSpacing = CategorySpacing,
|
||||
VerticalSpacing = CategorySpacing,
|
||||
TopPadding = 4.0f,
|
||||
BottomPadding = 4.0f,
|
||||
ContentHeight = 0f,
|
||||
AutoHideScrollBar = true,
|
||||
};
|
||||
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);
|
||||
|
||||
@@ -90,7 +94,6 @@ public unsafe class AddonRetainerWindow : InventoryAddonBase
|
||||
};
|
||||
_entrustDuplicatesButton.AttachNode(this);
|
||||
|
||||
// Slot counter
|
||||
_slotCounterNode = new TextNode
|
||||
{
|
||||
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);
|
||||
|
||||
protected override float MinWindowWidth => 400;
|
||||
protected override float MinWindowWidth => 500;
|
||||
protected override float MaxWindowWidth => 600;
|
||||
|
||||
protected override void OnSetup(AtkUnitBase* addon)
|
||||
@@ -31,16 +31,20 @@ public unsafe class AddonSaddleBagWindow : InventoryAddonBase
|
||||
|
||||
WindowNode?.AddColor = _tintColor;
|
||||
|
||||
CategoriesNode = new WrappingGridNode<InventoryCategoryNodeBase>
|
||||
ScrollableCategories = new ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>>
|
||||
{
|
||||
Position = ContentStartPosition,
|
||||
Size = ContentSize,
|
||||
HorizontalSpacing = CategorySpacing,
|
||||
VerticalSpacing = CategorySpacing,
|
||||
TopPadding = 4.0f,
|
||||
BottomPadding = 4.0f,
|
||||
ContentHeight = 0f,
|
||||
AutoHideScrollBar = true,
|
||||
};
|
||||
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);
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected readonly HashSet<InventoryCategoryNode> HoverSubscribed = new();
|
||||
|
||||
protected DragDropNode BackgroundDropTarget = null!;
|
||||
protected ScrollingAreaNode<WrappingGridNode<InventoryCategoryNodeBase>> ScrollableCategories = null!;
|
||||
protected WrappingGridNode<InventoryCategoryNodeBase> CategoriesNode = null!;
|
||||
protected TextInputWithButtonNode SearchInputNode = null!;
|
||||
protected InventoryFooterNode FooterNode = null!;
|
||||
@@ -37,6 +38,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
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 MaxWindowWidth => 800;
|
||||
protected virtual float MinWindowHeight => 200;
|
||||
@@ -47,11 +60,16 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
protected const float ItemPadding = 5;
|
||||
protected const float FooterHeight = 28f;
|
||||
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 RefreshAutosizeQueued;
|
||||
protected bool IsSetupComplete;
|
||||
private bool _deferredPopulationInProgress;
|
||||
private bool _initialPopulationComplete;
|
||||
private const int ItemsPerFrame = 50;
|
||||
|
||||
protected abstract InventoryStateBase InventoryState { get; }
|
||||
|
||||
@@ -61,6 +79,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
private readonly HashSet<uint> _searchMatchScratch = new();
|
||||
private bool _isRefreshing;
|
||||
private string _lastSearchText = string.Empty;
|
||||
|
||||
private int _requestedUpdateCount;
|
||||
private int _refreshFromLifecycleCount;
|
||||
@@ -72,6 +91,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
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)
|
||||
{
|
||||
Services.Framework.RunOnTick(() =>
|
||||
@@ -109,6 +135,12 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
string searchText = SearchInputNode.SearchString.ExtractText();
|
||||
bool isSearching = !string.IsNullOrWhiteSpace(searchText);
|
||||
|
||||
if (searchText != _lastSearchText)
|
||||
{
|
||||
_lastSearchText = searchText;
|
||||
System.AetherBagsAPI?.API.RaiseSearchChanged(searchText);
|
||||
}
|
||||
|
||||
if (config.SearchMode == SearchMode.Highlight && isSearching)
|
||||
{
|
||||
_searchMatchScratch.Clear();
|
||||
@@ -154,17 +186,20 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
float maxContentWidth = CategoriesNode.Width > 0 ? CategoriesNode.Width : ContentSize.X;
|
||||
int maxItemsPerLine = CalculateOptimalItemsPerLine(maxContentWidth);
|
||||
|
||||
bool deferItems = !_deferredPopulationInProgress && !_initialPopulationComplete;
|
||||
|
||||
CategoriesNode.SyncWithListDataByKey<CategorizedInventory, InventoryCategoryNode, uint>(
|
||||
dataList: categories,
|
||||
getKeyFromData: categorizedInventory => categorizedInventory.Key,
|
||||
getKeyFromNode: node => node.CategorizedInventory.Key,
|
||||
updateNode: (node, data) =>
|
||||
{
|
||||
node.MaxWidth = maxContentWidth;
|
||||
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine));
|
||||
node.RefreshNodeVisuals();
|
||||
node.SetCategoryData(data, Math.Min(data.Items.Count, maxItemsPerLine), deferItemCreation: deferItems);
|
||||
if (!deferItems) node.RefreshNodeVisuals();
|
||||
},
|
||||
createNodeMethod: _ => CreateCategoryNode(maxContentWidth));
|
||||
createNodeMethod: _ => CreateCategoryNode(),
|
||||
resetNodeForReuse: ResetCategoryNodeForReuse,
|
||||
externalPool: SharedCategoryNodePool);
|
||||
|
||||
if (HasPinning)
|
||||
{
|
||||
@@ -174,6 +209,8 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
WireHoverHandlers();
|
||||
|
||||
CategoriesNode.InvalidateLayout();
|
||||
|
||||
if (autosize)
|
||||
AutoSizeWindow();
|
||||
else
|
||||
@@ -181,6 +218,104 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
LayoutContent();
|
||||
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
|
||||
@@ -196,11 +331,38 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
var header = addon->WindowHeaderCollisionNode;
|
||||
float headerW = header->Width;
|
||||
|
||||
float settingsX = headerW - 62f;
|
||||
float itemY = header->Y + (header->Height - 28f) * 0.5f;
|
||||
|
||||
float searchWidth = headerW * 0.45f;
|
||||
float searchX = (headerW - searchWidth) * 0.5f;
|
||||
// Reserve space for close button (~50px) and settings button (~48px + gap)
|
||||
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
|
||||
{
|
||||
@@ -231,17 +393,29 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
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 },
|
||||
MaxWidth = maxWidth,
|
||||
OnRefreshRequested = ManualRefresh,
|
||||
OnDragEnd = () => InventoryOrchestrator.RefreshAll(updateMaps: true),
|
||||
SharedItemPool = SharedItemNodePool,
|
||||
};
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (!acceptedPayload.IsValidInventoryPayload) return;
|
||||
@@ -316,17 +490,20 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
float gridH = contentSize.Y - (HasFooter ? FooterHeight + FooterTopSpacing : 0);
|
||||
if (gridH < 0) gridH = 0;
|
||||
|
||||
CategoriesNode.Position = contentPos;
|
||||
CategoriesNode.Size = new Vector2(contentSize.X, gridH);
|
||||
ScrollableCategories.Position = contentPos;
|
||||
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)
|
||||
{
|
||||
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.RecalculateSize();
|
||||
@@ -343,7 +520,7 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
for (int i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
if (nodes[i] is not InventoryCategoryNode cat)
|
||||
if (nodes[i] is not InventoryCategoryNodeBase cat)
|
||||
continue;
|
||||
|
||||
childCount++;
|
||||
@@ -354,36 +531,68 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
if (childCount == 0)
|
||||
{
|
||||
ResizeWindow(MinWindowWidth, MinWindowHeight, recalcLayout: true);
|
||||
UpdateScrollParameters();
|
||||
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);
|
||||
|
||||
if (SettingsButtonNode != null)
|
||||
{
|
||||
SettingsButtonNode.X = finalWidth - 62f;
|
||||
SettingsButtonNode.X = finalWidth - SettingsButtonOffset;
|
||||
}
|
||||
|
||||
float contentWidth = finalWidth - (ContentStartPosition.X * 2);
|
||||
float categoriesWidth = contentWidth - ScrollBarWidth;
|
||||
|
||||
float footerSpace = HasFooter || HasSlotCounter ? FooterHeight + FooterTopSpacing : 0;
|
||||
float gridBudget = Math.Max(0f, MaxWindowHeight - footerSpace);
|
||||
|
||||
CategoriesNode.Position = ContentStartPosition;
|
||||
CategoriesNode.Size = new Vector2(contentWidth, gridBudget);
|
||||
|
||||
UpdateCategoryMaxWidths(contentWidth);
|
||||
|
||||
CategoriesNode.Width = categoriesWidth;
|
||||
UpdateCategoryMaxWidths(categoriesWidth);
|
||||
CategoriesNode.RecalculateLayout();
|
||||
|
||||
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);
|
||||
|
||||
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)
|
||||
@@ -395,10 +604,32 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
BackgroundDropTarget.Size = ContentSize;
|
||||
}
|
||||
|
||||
UpdateHeaderLayout();
|
||||
LayoutContent();
|
||||
|
||||
if (recalcLayout)
|
||||
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)
|
||||
@@ -438,6 +669,13 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
{
|
||||
ContextMenu = new ContextMenu();
|
||||
|
||||
System.AetherBagsAPI?.API.RaiseInventoryOpened();
|
||||
|
||||
if (ScrollableCategories != null)
|
||||
{
|
||||
ScrollableCategories.ScrollBarNode.OnValueChanged = OnScrollValueChanged;
|
||||
}
|
||||
|
||||
base.OnSetup(addon);
|
||||
}
|
||||
|
||||
@@ -457,10 +695,18 @@ public abstract unsafe class InventoryAddonBase : NativeAddon, IInventoryWindow
|
||||
|
||||
protected override void OnFinalize(AtkUnitBase* addon)
|
||||
{
|
||||
System.AetherBagsAPI?.API.RaiseInventoryClosed();
|
||||
|
||||
ContextMenu?.Dispose();
|
||||
HoverSubscribed.Clear();
|
||||
RefreshQueued = false;
|
||||
RefreshAutosizeQueued = false;
|
||||
_deferredPopulationInProgress = false;
|
||||
_initialPopulationComplete = false;
|
||||
|
||||
SharedItemNodePool.Clear();
|
||||
SharedCategoryNodePool.Clear();
|
||||
CategoryVirtualization.ClearLayout();
|
||||
|
||||
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 ShowCategoryItemCount { get; set; } = false;
|
||||
public bool LinkItemEnabled { get; set; } = false;
|
||||
public bool UseUnifiedExternalCategories { get; set; } = false;
|
||||
}
|
||||
|
||||
public enum InventoryStackMode : byte
|
||||
|
||||
@@ -2,7 +2,7 @@ using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Lumina.Excel.Sheets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
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, CurrencyStaticInfo> CurrencyStaticByItemIdCache = new(capacity: 64);
|
||||
private static readonly List<CurrencyInfo> CurrencyInfoScratch = new(capacity: 8);
|
||||
|
||||
private static uint? _cachedLimitedTomestoneItemId;
|
||||
private static uint? _cachedNonLimitedTomestoneItemId;
|
||||
@@ -29,6 +30,12 @@ public static unsafe class CurrencyState
|
||||
}
|
||||
|
||||
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)
|
||||
return Array.Empty<CurrencyInfo>();
|
||||
@@ -37,7 +44,7 @@ public static unsafe class CurrencyState
|
||||
if (inventoryManager == null)
|
||||
return Array.Empty<CurrencyInfo>();
|
||||
|
||||
List<CurrencyInfo> currencyInfoList = new List<CurrencyInfo>(currencyIds.Length);
|
||||
CurrencyInfoScratch.Clear();
|
||||
|
||||
for (int i = 0; i < currencyIds.Length; i++)
|
||||
{
|
||||
@@ -57,7 +64,7 @@ public static unsafe class CurrencyState
|
||||
isCapped = weeklyAcquired >= weeklyLimit;
|
||||
}
|
||||
|
||||
currencyInfoList.Add(new CurrencyInfo
|
||||
CurrencyInfoScratch.Add(new CurrencyInfo
|
||||
{
|
||||
Amount = amount,
|
||||
MaxAmount = staticInfo.MaxAmount,
|
||||
@@ -68,7 +75,7 @@ public static unsafe class CurrencyState
|
||||
});
|
||||
}
|
||||
|
||||
return currencyInfoList;
|
||||
return CurrencyInfoScratch;
|
||||
}
|
||||
|
||||
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.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.IPC.ExternalCategorySystem;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace AetherBags.IPC;
|
||||
|
||||
@@ -188,8 +192,119 @@ public class AllaganToolsIPC : IDisposable
|
||||
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()
|
||||
{
|
||||
DisableExternalCategorySupport();
|
||||
_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.Numerics;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.IPC.ExternalCategorySystem;
|
||||
using Dalamud.Plugin.Ipc;
|
||||
using KamiToolKit.Classes;
|
||||
|
||||
namespace AetherBags.IPC;
|
||||
|
||||
@@ -190,9 +193,157 @@ public class BisBuddyIPC : IDisposable
|
||||
public Vector4? GetItemColor(uint itemId)
|
||||
=> 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()
|
||||
{
|
||||
DisableExternalCategorySupport();
|
||||
_initialized?.Unsubscribe(OnBisBuddyInitialized);
|
||||
_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 AetherBags.Configuration;
|
||||
|
||||
namespace AetherBags.IPC;
|
||||
|
||||
@@ -8,6 +9,42 @@ public class IPCService : IDisposable
|
||||
public WotsItIPC WotsIt { 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()
|
||||
{
|
||||
AllaganTools.Dispose();
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory.Items;
|
||||
using KamiToolKit.Classes;
|
||||
@@ -61,32 +63,24 @@ public static class CategoryBucketManager
|
||||
{
|
||||
sortedScratch.Clear();
|
||||
sortedScratch.AddRange(userCategories);
|
||||
sortedScratch.Sort((left, right) =>
|
||||
{
|
||||
int priority = left.Priority.CompareTo(right.Priority);
|
||||
if (priority != 0) return priority;
|
||||
sortedScratch.Sort(UserCategoryComparer.Instance);
|
||||
|
||||
int order = left.Order.CompareTo(right.Order);
|
||||
if (order != 0) return order;
|
||||
|
||||
return string.Compare(left.Id, right.Id, StringComparison.OrdinalIgnoreCase);
|
||||
});
|
||||
var activeBuckets = new (uint key, CategoryBucket bucket, UserCategoryDefinition def)[sortedScratch.Count];
|
||||
int activeCount = 0;
|
||||
|
||||
for (int i = 0; i < sortedScratch.Count; i++)
|
||||
{
|
||||
UserCategoryDefinition category = sortedScratch[i];
|
||||
|
||||
if (!category.Enabled)
|
||||
continue;
|
||||
|
||||
if (UserCategoryMatcher.IsCatchAll(category))
|
||||
if (!category.Enabled || UserCategoryMatcher.IsCatchAll(category))
|
||||
continue;
|
||||
|
||||
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,
|
||||
Category = new CategoryInfo
|
||||
@@ -100,34 +94,63 @@ public static class CategoryBucketManager
|
||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||
Used = true,
|
||||
};
|
||||
bucketsByKey.Add(bucketKey, bucket);
|
||||
}
|
||||
else
|
||||
{
|
||||
bucket.Used = true;
|
||||
bucket.Category.Name = category.Name;
|
||||
bucket.Category.Description = category.Description;
|
||||
bucket.Category.Color = category.Color;
|
||||
bucket.Category.IsPinned = category.Pinned;
|
||||
bucketRef!.Used = true;
|
||||
bucketRef.Category.Name = category.Name;
|
||||
bucketRef.Category.Description = category.Description;
|
||||
bucketRef.Category.Color = category.Color;
|
||||
bucketRef.Category.IsPinned = category.Pinned;
|
||||
}
|
||||
|
||||
activeBuckets[activeCount++] = (bucketKey, bucketRef!, category);
|
||||
}
|
||||
|
||||
foreach (var itemKvp in itemInfoByKey)
|
||||
{
|
||||
ulong itemKey = itemKvp.Key;
|
||||
ItemInfo item = itemKvp.Value;
|
||||
|
||||
if (claimedKeys.Contains(itemKey))
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (bucket.Items.Count == 0)
|
||||
bucket.Used = false;
|
||||
for (int i = 0; i < activeCount; i++)
|
||||
{
|
||||
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;
|
||||
|
||||
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,
|
||||
Category = GetCategoryInfoCached(categoryKey, info),
|
||||
@@ -157,14 +182,13 @@ public static class CategoryBucketManager
|
||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||
Used = true,
|
||||
};
|
||||
bucketsByKey.Add(categoryKey, bucket);
|
||||
}
|
||||
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;
|
||||
|
||||
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;
|
||||
foreach (var (filterKey, filterName) in filters)
|
||||
foreach (var filterKey in filters.Keys)
|
||||
{
|
||||
if (!filterItems. TryGetValue(filterKey, out var itemIds))
|
||||
{
|
||||
index++;
|
||||
continue;
|
||||
filterKeyToIndex[filterKey] = index++;
|
||||
}
|
||||
|
||||
uint bucketKey = MakeAllaganFilterKey(index);
|
||||
|
||||
if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket))
|
||||
index = 0;
|
||||
foreach (var (filterKey, filterName) in filters)
|
||||
{
|
||||
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,
|
||||
Category = new CategoryInfo
|
||||
@@ -206,33 +234,44 @@ public static class CategoryBucketManager
|
||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||
Used = true,
|
||||
};
|
||||
bucketsByKey. Add(bucketKey, bucket);
|
||||
}
|
||||
else
|
||||
{
|
||||
bucket.Used = true;
|
||||
bucket.Category.Name = $"[AT] {filterName}";
|
||||
bucketRef!.Used = true;
|
||||
bucketRef.Category.Name = $"[AT] {filterName}";
|
||||
}
|
||||
|
||||
index++;
|
||||
}
|
||||
|
||||
foreach (var itemKvp in itemInfoByKey)
|
||||
{
|
||||
ulong itemKey = itemKvp.Key;
|
||||
ItemInfo item = itemKvp.Value;
|
||||
|
||||
if (claimedKeys.Contains(itemKey))
|
||||
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);
|
||||
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;
|
||||
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -250,9 +289,11 @@ public static class CategoryBucketManager
|
||||
|
||||
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,
|
||||
Category = new CategoryInfo
|
||||
@@ -265,21 +306,22 @@ public static class CategoryBucketManager
|
||||
FilteredItems = new List<ItemInfo>(capacity: 16),
|
||||
Used = true,
|
||||
};
|
||||
bucketsByKey.Add(bucketKey, bucket);
|
||||
}
|
||||
else
|
||||
{
|
||||
bucket.Used = true;
|
||||
bucketRef!.Used = true;
|
||||
}
|
||||
|
||||
var bucket = bucketRef!;
|
||||
|
||||
foreach (var itemKvp in itemInfoByKey)
|
||||
{
|
||||
ulong itemKey = itemKvp.Key;
|
||||
ItemInfo item = itemKvp.Value;
|
||||
|
||||
if (claimedKeys.Contains(itemKey))
|
||||
continue;
|
||||
|
||||
ItemInfo item = itemKvp.Value;
|
||||
|
||||
if (bisItems.ContainsKey(item.Item.ItemId))
|
||||
{
|
||||
bucket.Items.Add(item);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Inventory.Items;
|
||||
@@ -7,51 +8,20 @@ namespace AetherBags.Inventory.Categories;
|
||||
|
||||
internal static class UserCategoryMatcher
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool Matches(ItemInfo item, UserCategoryDefinition userCategory)
|
||||
{
|
||||
var rules = userCategory.Rules;
|
||||
|
||||
bool hasIdentificationFilters = rules.AllowedItemIds.Count > 0 || rules.AllowedItemNamePatterns.Count > 0;
|
||||
|
||||
if (hasIdentificationFilters)
|
||||
{
|
||||
bool matchesAnyIdentification = false;
|
||||
|
||||
if (rules.AllowedItemIds.Count > 0 && rules.AllowedItemIds.Contains(item.Item.ItemId))
|
||||
{
|
||||
matchesAnyIdentification = true;
|
||||
}
|
||||
|
||||
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 (!MatchesToggle(rules.Untradable, item.IsUntradable)) return false;
|
||||
if (!MatchesToggle(rules.Unique, item.IsUnique)) return false;
|
||||
if (!MatchesToggle(rules.Collectable, item.IsCollectable)) return false;
|
||||
if (!MatchesToggle(rules.Dyeable, item.IsDyeable)) return false;
|
||||
if (!MatchesToggle(rules.HighQuality, item.IsHq)) return false;
|
||||
if (!MatchesToggle(rules.Repairable, item.IsRepairable)) return false;
|
||||
if (!MatchesToggle(rules.Desynthesizable, item.IsDesynthesizable)) return false;
|
||||
if (!MatchesToggle(rules.Glamourable, item.IsGlamourable)) return false;
|
||||
if (!MatchesToggle(rules.FullySpiritbonded, item.IsSpiritbonded)) return false;
|
||||
|
||||
if (rules.Level.Enabled && !InRange(item.Level, rules.Level.Min, rules.Level.Max))
|
||||
return false;
|
||||
@@ -62,19 +32,40 @@ internal static class UserCategoryMatcher
|
||||
if (rules.VendorPrice.Enabled && !InRange(item.VendorPrice, rules.VendorPrice.Min, rules.VendorPrice.Max))
|
||||
return false;
|
||||
|
||||
if (!MatchesToggle(rules.Untradable, item.IsUntradable)) return false;
|
||||
if (!MatchesToggle(rules.Unique, item.IsUnique)) return false;
|
||||
if (!MatchesToggle(rules.Collectable, item.IsCollectable)) return false;
|
||||
if (!MatchesToggle(rules.Dyeable, item.IsDyeable)) return false;
|
||||
if (!MatchesToggle(rules.Repairable, item.IsRepairable)) return false;
|
||||
if (!MatchesToggle(rules.HighQuality, item.IsHq)) return false;
|
||||
if (!MatchesToggle(rules.Desynthesizable, item.IsDesynthesizable)) return false;
|
||||
if (!MatchesToggle(rules.Glamourable, item.IsGlamourable)) return false;
|
||||
if (!MatchesToggle(rules.FullySpiritbonded, item.IsSpiritbonded)) return false;
|
||||
if (rules.AllowedRarities.Count > 0 && !rules.AllowedRarities.Contains(item.Rarity))
|
||||
return false;
|
||||
|
||||
if (rules.AllowedUiCategoryIds.Count > 0 && !rules.AllowedUiCategoryIds.Contains(item.UiCategory.RowId))
|
||||
return false;
|
||||
|
||||
bool hasIdentificationFilters = rules.AllowedItemIds.Count > 0 || rules.AllowedItemNamePatterns.Count > 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool InRange<T>(T value, T min, T max) where T : struct, IComparable<T>
|
||||
=> value.CompareTo(min) >= 0 && value.CompareTo(max) <= 0;
|
||||
|
||||
@@ -112,12 +103,13 @@ internal static class UserCategoryMatcher
|
||||
return true;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool MatchesToggle(StateFilter filter, bool itemHasProperty)
|
||||
=> filter.ToggleState switch
|
||||
{
|
||||
ToggleFilterState.Ignored => true,
|
||||
ToggleFilterState.Allow => itemHasProperty,
|
||||
ToggleFilterState.Disallow => !itemHasProperty,
|
||||
_ => true
|
||||
};
|
||||
var state = filter.ToggleState;
|
||||
if (state == ToggleFilterState.Ignored) return true;
|
||||
if (state == ToggleFilterState.Allow) return itemHasProperty;
|
||||
if (state == ToggleFilterState.Disallow) return !itemHasProperty;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace AetherBags.Inventory.Context;
|
||||
|
||||
@@ -8,6 +9,7 @@ public enum HighlightSource
|
||||
Search,
|
||||
AllaganTools,
|
||||
BiSBuddy,
|
||||
Relationship,
|
||||
}
|
||||
|
||||
public record HighlightEntry(uint ItemId, Vector3 Color);
|
||||
@@ -40,6 +42,7 @@ public static class HighlightState
|
||||
_version++;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool IsInActiveFilters(uint itemId)
|
||||
{
|
||||
if (Filters.Count == 0) return true;
|
||||
@@ -48,6 +51,7 @@ public static class HighlightState
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static HighlightEntry? GetHighlightEntry(uint itemId)
|
||||
{
|
||||
EnsureCacheValid();
|
||||
@@ -88,6 +92,7 @@ public static class HighlightState
|
||||
_version++;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static Vector3? GetLabelColor(uint itemId)
|
||||
=> GetHighlightEntry(itemId)?.Color;
|
||||
|
||||
@@ -168,4 +173,16 @@ public static class HighlightState
|
||||
PerItemLabels.Remove(source);
|
||||
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.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using AetherBags.Helpers;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.IPC.ExternalCategorySystem;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using Lumina.Excel;
|
||||
using Lumina.Excel.Sheets;
|
||||
@@ -30,6 +32,7 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||
private int _cachedHighlightVersion = -1;
|
||||
private float _cachedVisualAlpha;
|
||||
private Vector3 _cachedHighlightColor;
|
||||
private bool _cachedIsRelationshipHighlighted;
|
||||
|
||||
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()
|
||||
{
|
||||
int currentVersion = HighlightState.Version;
|
||||
@@ -127,6 +139,10 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||
_cachedHighlightColor = System.Config.Categories.BisBuddyEnabled
|
||||
? HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero
|
||||
: Vector3.Zero;
|
||||
|
||||
var entry = HighlightState.GetHighlightEntry(Item.ItemId);
|
||||
_cachedIsRelationshipHighlighted = entry != null;
|
||||
|
||||
_cachedHighlightVersion = currentVersion;
|
||||
}
|
||||
|
||||
@@ -160,6 +176,7 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||
|
||||
public bool IsMainInventory => InventoryPage >= 0;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsRegexMatch(string searchTerms)
|
||||
{
|
||||
if (string.IsNullOrEmpty(searchTerms))
|
||||
@@ -171,22 +188,23 @@ public sealed class ItemInfo : IEquatable<ItemInfo>
|
||||
|
||||
if (re.IsMatch(Name)) return true;
|
||||
|
||||
if (re.IsMatch(Description)) return true;
|
||||
|
||||
if (re.IsMatch(LevelString)) return true;
|
||||
if (re.IsMatch(ItemLevelString)) return true;
|
||||
|
||||
if (ExternalCategoryManager.MatchesSearchTag(Item.ItemId, searchTerms)) return true;
|
||||
|
||||
if (re.IsMatch(Description)) return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool IsRegexMatch(Regex re)
|
||||
{
|
||||
if (re.IsMatch(Name)) return true;
|
||||
if (re.IsMatch(Description)) return true;
|
||||
|
||||
if (re.IsMatch(LevelString)) return true;
|
||||
if (re.IsMatch(ItemLevelString)) return true;
|
||||
|
||||
if (re.IsMatch(Description)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using AetherBags.Inventory.Categories;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.Inventory.Scanning;
|
||||
using AetherBags.IPC.ExternalCategorySystem;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
|
||||
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 (config.Categories.AllaganToolsFilterMode == PluginFilterMode.Categorize)
|
||||
@@ -120,6 +139,7 @@ public abstract class InventoryStateBase
|
||||
{
|
||||
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
||||
}
|
||||
}
|
||||
|
||||
if (gameCategoriesEnabled)
|
||||
{
|
||||
@@ -205,6 +225,9 @@ public abstract class InventoryStateBase
|
||||
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(uint[] currencyIds)
|
||||
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
||||
|
||||
public static IReadOnlyList<CurrencyInfo> GetCurrencyInfoList(List<uint> currencyIds)
|
||||
=> CurrencyState.GetCurrencyInfoList(currencyIds);
|
||||
|
||||
public static void InvalidateCurrencyCaches()
|
||||
=> CurrencyState.InvalidateCaches();
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ public class InventoryMonitor : IDisposable
|
||||
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, saddle, OnPreFinalize);
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, retainer, OnPreFinalize);
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreFinalize, bags, OnInventoryPreFinalize);
|
||||
|
||||
// PreRefresh Handlers
|
||||
Services.AddonLifecycle.RegisterListener(AddonEvent.PreRefresh, bags, InventoryPreRefreshHandler);
|
||||
@@ -65,6 +66,11 @@ public class InventoryMonitor : IDisposable
|
||||
OpenInventories(args.AddonName);
|
||||
}
|
||||
|
||||
private void OnInventoryPreFinalize(AddonEvent type, AddonArgs args)
|
||||
{
|
||||
System.AddonInventoryWindow.Close();
|
||||
}
|
||||
|
||||
private unsafe void OpenInventories(string name)
|
||||
{
|
||||
GeneralSettings config = System.Config.General;
|
||||
@@ -168,22 +174,27 @@ public class InventoryMonitor : IDisposable
|
||||
|
||||
if (atkValues.Length < 7) return;
|
||||
|
||||
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
|
||||
AtkValue* value5 = (AtkValue*)atkValues[5].Address;
|
||||
AtkValue* value6 = (AtkValue*)atkValues[6].Address;
|
||||
|
||||
if (value5->Type != ValueType.ManagedString || value6->Type != ValueType.ManagedString)
|
||||
return;
|
||||
|
||||
int openTitleId = value1->Int;
|
||||
ReadOnlySeString title = value5->String.AsReadOnlySeString();
|
||||
ReadOnlySeString upperTitle = value6->String.AsReadOnlySeString();
|
||||
|
||||
System.AddonInventoryWindow.SetNotification(new InventoryNotificationInfo(title, upperTitle));
|
||||
|
||||
if (config.HideGameInventory) refreshArgs.AtkValueCount = 0;
|
||||
if (config.HideGameInventory)
|
||||
{
|
||||
refreshArgs.AtkValueCount = 0;
|
||||
}
|
||||
|
||||
if (config.OpenWithGameInventory)
|
||||
{
|
||||
AtkValue* value1 = (AtkValue*)atkValues[1].Address;
|
||||
int openTitleId = value1->Int;
|
||||
|
||||
if (openTitleId == 0)
|
||||
{
|
||||
System.AddonInventoryWindow.Toggle();
|
||||
@@ -234,6 +245,6 @@ public class InventoryMonitor : IDisposable
|
||||
public void Dispose()
|
||||
{
|
||||
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 long _batchStartTick;
|
||||
private bool _hasPendingRemoval;
|
||||
private int _nextIndex;
|
||||
|
||||
public event Action<IReadOnlyList<LootedItemInfo>>? OnLootedItemsChanged;
|
||||
|
||||
@@ -32,8 +33,6 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
|
||||
public bool HasPendingChanges => _pendingChanges.Count > 0 || _hasPendingRemoval;
|
||||
|
||||
private int GetNextIndex() => _lootedItems.Count > 0 ? _lootedItems.Max(x => x.Index) + 1 : 0;
|
||||
|
||||
public void Enable()
|
||||
{
|
||||
if (_isEnabled) return;
|
||||
@@ -43,6 +42,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
_pendingChanges.Clear();
|
||||
_batchStartTick = 0;
|
||||
_hasPendingRemoval = false;
|
||||
_nextIndex = 0;
|
||||
Services.GameInventory.InventoryChangedRaw += OnInventoryChangedRaw;
|
||||
Services.Framework.Update += OnFrameworkUpdate;
|
||||
}
|
||||
@@ -58,12 +58,14 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
_pendingChanges.Clear();
|
||||
_batchStartTick = 0;
|
||||
_hasPendingRemoval = false;
|
||||
_nextIndex = 0;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_lootedItems.Clear();
|
||||
_hasPendingRemoval = true;
|
||||
_nextIndex = 0;
|
||||
}
|
||||
|
||||
public void RemoveByIndex(int index)
|
||||
@@ -100,9 +102,7 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
|
||||
foreach (var ((itemId, isHq), (item, delta)) in _pendingChanges)
|
||||
{
|
||||
int existingIndex = _lootedItems.FindIndex(x =>
|
||||
x.Item.ItemId == itemId &&
|
||||
x.Item.Flags.HasFlag(InventoryItem.ItemFlags.HighQuality) == isHq);
|
||||
int existingIndex = FindExistingItemIndex(itemId, isHq);
|
||||
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
@@ -116,13 +116,27 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
}
|
||||
else if (delta > 0)
|
||||
{
|
||||
_lootedItems.Add(new LootedItemInfo(GetNextIndex(), item, delta));
|
||||
_lootedItems.Add(new LootedItemInfo(_nextIndex++, item, delta));
|
||||
}
|
||||
}
|
||||
|
||||
_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)
|
||||
{
|
||||
if (!_isEnabled || !Services.ClientState.IsLoggedIn) return;
|
||||
@@ -156,7 +170,12 @@ public sealed unsafe class LootedItemsTracker : IDisposable
|
||||
|
||||
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
|
||||
{
|
||||
|
||||
@@ -41,6 +41,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
||||
OnClick = isChecked =>
|
||||
{
|
||||
config.CategoriesEnabled = isChecked;
|
||||
System.IPC?.RefreshExternalSources();
|
||||
RefreshInventory();
|
||||
}
|
||||
};
|
||||
@@ -92,8 +93,9 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
||||
{
|
||||
config.BisBuddyMode = selected;
|
||||
if (selected == PluginFilterMode.Categorize)
|
||||
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
||||
HighlightState.ClearFilter(HighlightSource.BiSBuddy);
|
||||
|
||||
System.IPC?.RefreshExternalSources();
|
||||
RefreshInventory();
|
||||
}
|
||||
};
|
||||
@@ -110,6 +112,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
||||
config.BisBuddyEnabled = isChecked;
|
||||
if (bbModeDropdown != null) bbModeDropdown.IsEnabled = isChecked;
|
||||
if (isChecked) System.IPC.BisBuddy?.RefreshItems();
|
||||
System.IPC?.RefreshExternalSources();
|
||||
RefreshInventory();
|
||||
}
|
||||
};
|
||||
@@ -134,6 +137,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
||||
HighlightState.ClearFilter(HighlightSource.AllaganTools);
|
||||
}
|
||||
|
||||
System.IPC?.RefreshExternalSources();
|
||||
RefreshInventory();
|
||||
}
|
||||
};
|
||||
@@ -153,6 +157,7 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode
|
||||
config.AllaganToolsCategoriesEnabled = isChecked;
|
||||
if (atModeDropdown != null) atModeDropdown.IsEnabled = isChecked;
|
||||
if (isChecked) System.IPC?.AllaganTools?.RefreshFilters();
|
||||
System.IPC?.RefreshExternalSources();
|
||||
RefreshInventory();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -14,6 +14,8 @@ public sealed class CategoryScrollingAreaNode : ScrollingListNode
|
||||
|
||||
AddNode(new CategoryGeneralConfigurationNode());
|
||||
|
||||
AddNode(new ExperimentalConfigurationNode());
|
||||
|
||||
var categoryConfigurationButtonNode = new TextButtonNode
|
||||
{
|
||||
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 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 MinWidth = 40;
|
||||
|
||||
@@ -41,12 +42,16 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
private int _lastItemCount;
|
||||
private ulong _lastItemsHash;
|
||||
private int _lastItemsPerLine;
|
||||
private float? _lastMaxWidth;
|
||||
private bool _itemsNeedPopulation;
|
||||
|
||||
public event Action<InventoryCategoryNode, bool>? HeaderHoverChanged;
|
||||
|
||||
public bool NeedsItemPopulation => _itemsNeedPopulation;
|
||||
public Action? OnRefreshRequested { get; set; }
|
||||
public Action? OnDragEnd { get; set; }
|
||||
|
||||
public SharedNodePool<InventoryDragDropNode>? SharedItemPool { get; set; }
|
||||
|
||||
public InventoryCategoryNode()
|
||||
{
|
||||
_categoryNameTextNode = new TextNode
|
||||
@@ -86,11 +91,10 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
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 itemsPerLineChanged = itemsPerLine != _lastItemsPerLine;
|
||||
bool maxWidthChanged = _maxWidth != _lastMaxWidth;
|
||||
|
||||
ulong itemsHash = ComputeItemsHash(CollectionsMarshal.AsSpan(data.Items));
|
||||
bool itemsChanged = data.Items.Count != _lastItemCount || itemsHash != _lastItemsHash;
|
||||
@@ -99,7 +103,6 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
_lastItemCount = data.Items.Count;
|
||||
_lastItemsHash = itemsHash;
|
||||
_lastItemsPerLine = itemsPerLine;
|
||||
_lastMaxWidth = _maxWidth;
|
||||
|
||||
_categorizedInventory = data;
|
||||
|
||||
@@ -112,24 +115,45 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
_categoryNameTextNode.TextTooltip = data.Category.Description;
|
||||
|
||||
if (itemsChanged || categoryChanged)
|
||||
{
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
|
||||
if (deferItemCreation)
|
||||
{
|
||||
_itemsNeedPopulation = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
using (_itemGridNode.DeferRecalculateLayout())
|
||||
{
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
UpdateItemGrid();
|
||||
}
|
||||
_itemsNeedPopulation = false;
|
||||
}
|
||||
}
|
||||
else if (itemsPerLineChanged)
|
||||
{
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
}
|
||||
|
||||
if (categoryChanged || itemsChanged || itemsPerLineChanged || maxWidthChanged)
|
||||
if (categoryChanged || itemsChanged || itemsPerLineChanged)
|
||||
{
|
||||
RecalculateSize();
|
||||
}
|
||||
}
|
||||
|
||||
public void PopulateItems()
|
||||
{
|
||||
if (!_itemsNeedPopulation)
|
||||
return;
|
||||
|
||||
using (_itemGridNode.DeferRecalculateLayout())
|
||||
{
|
||||
UpdateItemGrid();
|
||||
}
|
||||
_itemsNeedPopulation = false;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static ulong ComputeItemsHash(ReadOnlySpan<ItemInfo> items)
|
||||
{
|
||||
@@ -164,7 +188,7 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
}
|
||||
}
|
||||
|
||||
public float? MaxWidth
|
||||
public override float? MaxWidth
|
||||
{
|
||||
get => _maxWidth;
|
||||
set => _maxWidth = value;
|
||||
@@ -234,12 +258,12 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
}
|
||||
}
|
||||
|
||||
public void RecalculateSize()
|
||||
public override void RecalculateSize()
|
||||
{
|
||||
int itemCount = CategorizedInventory.Items.Count;
|
||||
|
||||
float cellW = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Width : FallbackItemSize;
|
||||
float cellH = _itemGridNode.Nodes.Count > 0 ? _itemGridNode.Nodes[0].Height : FallbackItemSize;
|
||||
float cellW = ExpectedItemWidth;
|
||||
float cellH = ExpectedItemHeight;
|
||||
float hPad = _itemGridNode.HorizontalPadding;
|
||||
float vPad = _itemGridNode.VerticalPadding;
|
||||
|
||||
@@ -298,31 +322,55 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
getKeyFromData: item => item.Key,
|
||||
getKeyFromNode: node => node.ItemInfo?.Key ?? 0,
|
||||
updateNode: UpdateInventoryDragDropNode,
|
||||
createNodeMethod: CreateInventoryDragDropNode);
|
||||
createNodeMethod: CreateInventoryDragDropNode,
|
||||
resetNodeForReuse: ResetDragDropNodeForReuse,
|
||||
externalPool: SharedItemPool);
|
||||
}
|
||||
|
||||
private void UpdateInventoryDragDropNode(InventoryDragDropNode node, ItemInfo data)
|
||||
{
|
||||
if (node.ItemInfo?.Key == data.Key)
|
||||
{
|
||||
node.ItemInfo = data;
|
||||
node.Alpha = data.VisualAlpha;
|
||||
node.AddColor = data.HighlightOverlayColor;
|
||||
node.IsDraggable = !data.IsSlotBlocked;
|
||||
return;
|
||||
ApplyItemDataToNode(node, data);
|
||||
}
|
||||
|
||||
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;
|
||||
InventoryMappedLocation visualLocation = data.VisualLocation;
|
||||
|
||||
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
||||
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
||||
|
||||
node.ItemInfo = data;
|
||||
node.IconId = item.IconId;
|
||||
node.Alpha = data.VisualAlpha;
|
||||
node.AddColor = data.HighlightOverlayColor;
|
||||
node.IsDraggable = !data.IsSlotBlocked;
|
||||
node.IconNode.IconExtras.AntsNode.IsVisible = data.IsRelationshipHighlighted;
|
||||
node.Payload = new DragDropPayload
|
||||
{
|
||||
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;
|
||||
InventoryMappedLocation visualLocation = data.VisualLocation;
|
||||
if (n is not InventoryDragDropNode node) return;
|
||||
OnDiscard(n, node.ItemInfo);
|
||||
}
|
||||
|
||||
var visualInvType = InventoryType.GetInventoryTypeFromContainerId(visualLocation.Container);
|
||||
int absoluteIndex = visualInvType.GetInventoryStartIndex + visualLocation.Slot;
|
||||
private void OnNodePayloadAccepted(DragDropNode n, DragDropPayload acceptedPayload)
|
||||
{
|
||||
if (n is not InventoryDragDropNode node) return;
|
||||
OnPayloadAccepted(n, acceptedPayload, node.ItemInfo);
|
||||
}
|
||||
|
||||
DragDropPayload nodePayload = new DragDropPayload
|
||||
{
|
||||
// Int1 is always the container ID, for Item DragDrop Int2 is only used as a fallback
|
||||
// ReferenceIndex is the absolute index that's actually used
|
||||
Type = DragDropType.Item,
|
||||
Int1 = visualLocation.Container,
|
||||
Int2 = visualLocation.Slot,
|
||||
ReferenceIndex = (short)absoluteIndex
|
||||
};
|
||||
|
||||
return new InventoryDragDropNode
|
||||
{
|
||||
Size = new Vector2(42, 46),
|
||||
Alpha = data.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 =>
|
||||
private unsafe void OnNodeRollOver(DragDropNode n)
|
||||
{
|
||||
if (n is not InventoryDragDropNode node) return;
|
||||
BeginHeaderHover();
|
||||
node.ShowInventoryItemTooltip(item.Container, item.Slot);
|
||||
},
|
||||
OnRollOut = node =>
|
||||
var item = node.ItemInfo.Item;
|
||||
n.ShowInventoryItemTooltip(item.Container, item.Slot);
|
||||
}
|
||||
|
||||
private unsafe void OnNodeRollOut(DragDropNode n)
|
||||
{
|
||||
EndHeaderHover();
|
||||
|
||||
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(node)->Id;
|
||||
ushort addonId = RaptureAtkUnitManager.Instance()->GetAddonByNode(n)->Id;
|
||||
AtkStage.Instance()->TooltipManager.HideTooltip(addonId);
|
||||
},
|
||||
ItemInfo = data
|
||||
};
|
||||
}
|
||||
|
||||
public void RefreshNodeVisuals()
|
||||
@@ -392,6 +419,7 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
float newAlpha = info.VisualAlpha;
|
||||
Vector3 newColor = info.HighlightOverlayColor;
|
||||
bool newDraggable = !info.IsSlotBlocked;
|
||||
bool newAntsVisible = info.IsRelationshipHighlighted;
|
||||
|
||||
if (!NearlyEqual(itemNode.Alpha, newAlpha))
|
||||
itemNode.Alpha = newAlpha;
|
||||
@@ -401,6 +429,9 @@ public class InventoryCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
if (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");
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
/// </summary>
|
||||
public virtual bool IsPinnedInConfig => false;
|
||||
|
||||
public abstract float? MaxWidth { get; set; }
|
||||
|
||||
public abstract void RecalculateSize();
|
||||
}
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
using System.Numerics;
|
||||
using AetherBags.Addons;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.Inventory.Items;
|
||||
using AetherBags.IPC.ExternalCategorySystem;
|
||||
using Dalamud.Game.ClientState.Keys;
|
||||
using FFXIVClientStructs.FFXIV.Client.Game;
|
||||
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
|
||||
using FFXIVClientStructs.FFXIV.Component.GUI;
|
||||
using KamiToolKit.Classes;
|
||||
using KamiToolKit.Nodes;
|
||||
using KamiToolKit.Timelines;
|
||||
|
||||
namespace AetherBags.Nodes.Inventory;
|
||||
|
||||
public class InventoryDragDropNode : DragDropNode
|
||||
{
|
||||
private readonly TextNode _quantityTextNode;
|
||||
private IconNode? _badgeNode;
|
||||
private ImageNode? _borderNode;
|
||||
private ItemDecoration? _currentDecoration;
|
||||
|
||||
public unsafe InventoryDragDropNode()
|
||||
{
|
||||
_quantityTextNode = new TextNode {
|
||||
@@ -26,6 +34,8 @@ public class InventoryDragDropNode : DragDropNode
|
||||
_quantityTextNode.AttachNode(this);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseDown, OnItemMouseDown);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseClick, OnItemClicked);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseOver, OnItemHover);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseOut, OnItemUnhover);
|
||||
}
|
||||
|
||||
public required ItemInfo ItemInfo
|
||||
@@ -35,9 +45,164 @@ public class InventoryDragDropNode : DragDropNode
|
||||
{
|
||||
field = value;
|
||||
_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) {
|
||||
InventoryItem item = ItemInfo.Item;
|
||||
if (Services.KeyState[VirtualKey.SHIFT] && atkEventData->IsLeftClick && System.Config.General.LinkItemEnabled)
|
||||
@@ -48,6 +213,11 @@ public class InventoryDragDropNode : DragDropNode
|
||||
|
||||
if (!atkEventData->IsRightClick) return;
|
||||
|
||||
if (Services.KeyState[VirtualKey.CONTROL] && ItemContextMenuHandler.TryShowExternalMenu(ItemInfo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AgentInventoryContext* context = AgentInventoryContext.Instance();
|
||||
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;
|
||||
InventoryItem item = ItemInfo.Item;
|
||||
if (!atkEventData->IsLeftClick) return;
|
||||
|
||||
System.AetherBagsAPI?.API.RaiseItemClicked(item.ItemId);
|
||||
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;
|
||||
|
||||
//IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList([1, 28, 0xFFFF_FFFE, 0xFFFF_FFFD]);
|
||||
IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList(config.DisplayedCurrencies.ToArray());
|
||||
IReadOnlyList<CurrencyInfo> currencyInfoList = GetCurrencyInfoList(config.DisplayedCurrencies);
|
||||
_currencyListNode.SyncWithListDataByKey<CurrencyInfo, CurrencyNode, uint>(
|
||||
dataList: currencyInfoList,
|
||||
getKeyFromData: currencyInfo => currencyInfo.ItemId,
|
||||
|
||||
@@ -18,6 +18,8 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
private readonly TextNode _quantityTextNode;
|
||||
|
||||
public Action<LootedItemDisplayNode>? OnDismiss { get; set; }
|
||||
public Action<LootedItemDisplayNode>? OnRollOver { get; set; }
|
||||
public Action<LootedItemDisplayNode>? OnRollOut { get; set; }
|
||||
|
||||
public LootedItemDisplayNode()
|
||||
{
|
||||
@@ -28,9 +30,13 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
Position = new Vector2(0, 0),
|
||||
Size = new Vector2(42, 46),
|
||||
};
|
||||
_iconNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||
_iconNode.CollisionNode.NodeFlags = 0;
|
||||
_iconNode.AttachNode(this);
|
||||
|
||||
CollisionNode.AddEvent(AtkEventType.MouseClick, OnMouseClick);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseOver, OnMouseOver);
|
||||
CollisionNode.AddEvent(AtkEventType.MouseOut, OnMouseOut);
|
||||
|
||||
_quantityTextNode = new TextNode
|
||||
{
|
||||
Size = new Vector2(40.0f, 12.0f),
|
||||
@@ -80,4 +86,21 @@ public sealed unsafe class LootedItemDisplayNode : SimpleComponentNode
|
||||
if (!atkEventData->IsLeftClick) return;
|
||||
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 long _lastItemsHash;
|
||||
|
||||
private float? _maxWidth;
|
||||
|
||||
private int _hoverRefs;
|
||||
private bool _headerExpanded;
|
||||
private float _baseHeaderWidth = 96f;
|
||||
@@ -54,6 +56,12 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
public bool HasItems => _lootedItems.Count > 0;
|
||||
|
||||
public override float? MaxWidth
|
||||
{
|
||||
get => _maxWidth;
|
||||
set => _maxWidth = value;
|
||||
}
|
||||
|
||||
public LootedItemsCategoryNode()
|
||||
{
|
||||
_headerTextNode = new TextNode
|
||||
@@ -183,6 +191,8 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
Vector2 drawSize = _headerTextNode.GetTextDrawSize();
|
||||
float expandedWidth = MathF.Max(_baseHeaderWidth, drawSize.X + 4f);
|
||||
_headerTextNode.Size = _headerTextNode.Size with { X = expandedWidth };
|
||||
|
||||
_clearButton.Position = new Vector2(expandedWidth + 4f, (HeaderHeight - ClearButtonSize) / 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -193,6 +203,9 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
|
||||
flags |= TextFlags.OverflowHidden | TextFlags.Ellipsis;
|
||||
_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,
|
||||
getKeyFromNode: node => node.LootedItem?.Index ?? -1,
|
||||
updateNode: UpdateLootedItemNode,
|
||||
createNodeMethod: CreateLootedItemNode);
|
||||
createNodeMethod: CreateLootedItemNode,
|
||||
resetNodeForReuse: ResetLootedItemNodeForReuse);
|
||||
}
|
||||
|
||||
private static void UpdateLootedItemNode(LootedItemDisplayNode node, LootedItemInfo data)
|
||||
@@ -211,11 +225,18 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
node.LootedItem = data;
|
||||
}
|
||||
|
||||
private static void ResetLootedItemNodeForReuse(LootedItemDisplayNode node)
|
||||
{
|
||||
node.ResetForReuse();
|
||||
}
|
||||
|
||||
private LootedItemDisplayNode CreateLootedItemNode(LootedItemInfo lootedItem)
|
||||
{
|
||||
return new LootedItemDisplayNode
|
||||
{
|
||||
OnDismiss = OnItemDismissed,
|
||||
OnRollOver = _ => BeginHeaderHover(),
|
||||
OnRollOut = _ => EndHeaderHover(),
|
||||
LootedItem = lootedItem,
|
||||
};
|
||||
}
|
||||
@@ -227,13 +248,19 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
OnDismissItem?.Invoke(index);
|
||||
}
|
||||
|
||||
private void RecalculateSize()
|
||||
public override void RecalculateSize()
|
||||
{
|
||||
int itemCount = _lootedItems.Count;
|
||||
|
||||
const float cellW = 42f;
|
||||
const float cellH = 46f;
|
||||
|
||||
float hPad = _itemGridNode.HorizontalPadding;
|
||||
float vPad = _itemGridNode.VerticalPadding;
|
||||
|
||||
if (itemCount == 0)
|
||||
{
|
||||
float width = MinWidth;
|
||||
float width = _maxWidth.HasValue ? Math.Min(MinWidth, _maxWidth.Value) : MinWidth;
|
||||
Size = new Vector2(width, HeaderHeight);
|
||||
_baseHeaderWidth = width - ClearButtonSize - 4;
|
||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||
@@ -246,20 +273,36 @@ public class LootedItemsCategoryNode : InventoryCategoryNodeBase
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
if (_maxWidth.HasValue && _maxWidth.Value >= minUsableWidth)
|
||||
calculatedWidth = Math.Min(calculatedWidth, _maxWidth.Value);
|
||||
|
||||
float gridHeight = rows * cellH + (rows - 1) * vPad;
|
||||
float totalHeight = HeaderHeight + gridHeight;
|
||||
|
||||
Size = new Vector2(calculatedWidth, totalHeight);
|
||||
|
||||
if (_itemGridNode.ItemsPerLine != itemsPerLine)
|
||||
_itemGridNode.ItemsPerLine = itemsPerLine;
|
||||
|
||||
_baseHeaderWidth = calculatedWidth - ClearButtonSize - 4;
|
||||
_headerTextNode.Size = new Vector2(_baseHeaderWidth, HeaderHeight);
|
||||
_clearButton.Position = new Vector2(calculatedWidth - ClearButtonSize, (HeaderHeight - ClearButtonSize) / 2);
|
||||
|
||||
@@ -15,20 +15,75 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
private int _deferRecalcDepth;
|
||||
private bool _pendingRecalc;
|
||||
|
||||
/// <summary>
|
||||
/// Hide and detach a node from the UI tree without disposing it.
|
||||
/// Disposal happens later when KamiToolKit cleans up detached nodes.
|
||||
/// </summary>
|
||||
protected static void SafeDetachNode(NodeBase node)
|
||||
private readonly Dictionary<Type, Stack<NodeBase>> _nodePoolByType = new();
|
||||
private const int MaxPoolSizePerType = 64;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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
|
||||
{
|
||||
node.IsVisible = false;
|
||||
node.DetachNode();
|
||||
node.Dispose();
|
||||
}
|
||||
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;
|
||||
|
||||
NodeList.Remove(node);
|
||||
SafeDetachNode(node);
|
||||
SafeDisposeNode(node);
|
||||
|
||||
RecalculateLayout();
|
||||
}
|
||||
@@ -161,13 +216,16 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
{
|
||||
var node = NodeList[i];
|
||||
NodeList.RemoveAt(i);
|
||||
SafeDetachNode(node);
|
||||
SafeDisposeNode(node);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_suppressRecalculateLayout = false;
|
||||
}
|
||||
|
||||
DisposePool();
|
||||
|
||||
RecalculateLayout();
|
||||
}
|
||||
|
||||
@@ -248,13 +306,14 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
_dataKeysScratch = set;
|
||||
}
|
||||
|
||||
|
||||
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,
|
||||
Action<TU>? resetNodeForReuse = null,
|
||||
SharedNodePool<TU>? externalPool = null,
|
||||
IEqualityComparer<TKey>? keyComparer = null) where TU : NodeBase where TKey : notnull
|
||||
{
|
||||
keyComparer ??= EqualityComparer<TKey>.Default;
|
||||
@@ -295,9 +354,13 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
{
|
||||
for (int i = 0; i < toRemove.Count; i++)
|
||||
{
|
||||
var node = toRemove[i];
|
||||
var node = (TU)toRemove[i];
|
||||
NodeList.Remove(node);
|
||||
SafeDetachNode(node);
|
||||
|
||||
if (!TryReturnToPool(node, externalPool, resetNodeForReuse))
|
||||
{
|
||||
SafeDisposeNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -345,9 +408,20 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
}
|
||||
else
|
||||
{
|
||||
var newNode = createNodeMethod(data);
|
||||
NodeList.Add(newNode);
|
||||
TU newNode;
|
||||
var pooledNode = TryRentFromPool(externalPool);
|
||||
if (pooledNode != null)
|
||||
{
|
||||
newNode = pooledNode;
|
||||
newNode.AttachNode(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
newNode = createNodeMethod(data);
|
||||
newNode.AttachNode(this);
|
||||
}
|
||||
|
||||
NodeList.Add(newNode);
|
||||
updateNode(newNode, data);
|
||||
desired.Add(newNode);
|
||||
structureChanged = true;
|
||||
@@ -425,6 +499,15 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
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>(
|
||||
IEnumerable<T> dataList,
|
||||
GetDataFromNode<T?, TU> getDataFromNode,
|
||||
@@ -455,7 +538,7 @@ public abstract class DeferrableLayoutListNode : SimpleComponentNode
|
||||
if (nodeData is null || !dataSet.Contains(nodeData))
|
||||
{
|
||||
NodeList.Remove(tu);
|
||||
SafeDetachNode(tu);
|
||||
SafeDisposeNode(tu);
|
||||
anythingChanged = true;
|
||||
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[] _orderScratch = Array.Empty<int>();
|
||||
private bool _forceFullReflow;
|
||||
|
||||
private T? _hoistedNode;
|
||||
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> _pinnedScratch = new(capacity: 64);
|
||||
private readonly List<NodeBase> _normalScratch = new(capacity: 256);
|
||||
private readonly HashSet<T> _presentScratch = new(ReferenceEqualityComparer<T>.Instance);
|
||||
|
||||
public WrappingGridNode()
|
||||
{
|
||||
@@ -56,6 +58,11 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool TryGetRowIndex(NodeBase node, out int rowIndex) => _rowIndex.TryGetValue(node, out rowIndex);
|
||||
|
||||
public void InvalidateLayout()
|
||||
{
|
||||
_forceFullReflow = true;
|
||||
}
|
||||
|
||||
public void SetHoistedNode(T? node)
|
||||
{
|
||||
if (ReferenceEquals(_hoistedNode, node))
|
||||
@@ -111,10 +118,14 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
||||
_rowIndex.Clear();
|
||||
_requiredHeight = 0f;
|
||||
_requiredHeightDirty = false;
|
||||
_forceFullReflow = false;
|
||||
RememberLayoutParams();
|
||||
return;
|
||||
}
|
||||
|
||||
bool forceReflow = _forceFullReflow;
|
||||
_forceFullReflow = false;
|
||||
|
||||
bool hasSpecials = hoistedCount != 0 || pinnedCount != 0;
|
||||
bool compactEnabled = System.Config.General.CompactPackingEnabled;
|
||||
|
||||
@@ -128,7 +139,7 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rows.Count != 0 && LayoutParamsMatchLast() && NodeSetMatchesExistingLayout(layoutCount))
|
||||
if (!forceReflow && _rows.Count != 0 && LayoutParamsMatchLast() && NodeSetMatchesExistingLayout(layoutCount))
|
||||
{
|
||||
RepositionExistingRows();
|
||||
_requiredHeightDirty = true;
|
||||
@@ -142,7 +153,8 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rows.Count != 0 &&
|
||||
if (!forceReflow &&
|
||||
_rows.Count != 0 &&
|
||||
NodeSetMatchesExistingLayout(layoutCount) &&
|
||||
TryUpdateLayoutWithoutReflowOrTailReflow(layoutCount, hoistedCount, pinnedCount))
|
||||
{
|
||||
@@ -173,7 +185,8 @@ public sealed class WrappingGridNode<T> : DeferrableLayoutListNode where T : Nod
|
||||
return 0;
|
||||
}
|
||||
|
||||
var present = new HashSet<T>(ReferenceEqualityComparer<T>.Instance);
|
||||
_presentScratch.Clear();
|
||||
var present = _presentScratch;
|
||||
|
||||
bool hoistedPresent = false;
|
||||
T? hoisted = _hoistedNode;
|
||||
|
||||
@@ -6,6 +6,7 @@ using AetherBags.Hooks;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.Inventory.Context;
|
||||
using AetherBags.IPC;
|
||||
using AetherBags.IPC.AetherBagsAPI;
|
||||
using AetherBags.Monitoring;
|
||||
using Dalamud.Plugin;
|
||||
using KamiToolKit;
|
||||
@@ -29,6 +30,8 @@ public class Plugin : IDalamudPlugin
|
||||
KamiToolKitLibrary.Initialize(pluginInterface);
|
||||
|
||||
System.IPC = new IPCService();
|
||||
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||
ItemContextMenuHandler.Initialize();
|
||||
System.LootedItemsTracker = new LootedItemsTracker();
|
||||
|
||||
System.AddonInventoryWindow = new AddonInventoryWindow
|
||||
@@ -62,6 +65,8 @@ public class Plugin : IDalamudPlugin
|
||||
Services.PluginInterface.UiBuilder.OpenMainUi += System.AddonInventoryWindow.Toggle;
|
||||
Services.PluginInterface.UiBuilder.OpenConfigUi += System.AddonConfigurationWindow.Toggle;
|
||||
|
||||
System.AetherBagsAPI = new AetherBagsIPCProvider();
|
||||
|
||||
_commandHandler = new CommandHandler();
|
||||
|
||||
Services.ClientState.Login += OnLogin;
|
||||
@@ -78,10 +83,12 @@ public class Plugin : IDalamudPlugin
|
||||
public void Dispose()
|
||||
{
|
||||
InventoryAddonContextMenu.Close();
|
||||
ItemContextMenuHandler.Dispose();
|
||||
_inventoryHooks.Dispose();
|
||||
inventoryMonitor.Dispose();
|
||||
|
||||
System.LootedItemsTracker.Dispose();
|
||||
System.AetherBagsAPI?.Dispose();
|
||||
System.IPC.Dispose();
|
||||
HighlightState.ClearAll();
|
||||
|
||||
@@ -97,6 +104,7 @@ public class Plugin : IDalamudPlugin
|
||||
private void OnLogin()
|
||||
{
|
||||
System.Config = Util.LoadConfigOrDefault();
|
||||
System.IPC.UpdateUnifiedCategorySupport(System.Config.General.UseUnifiedExternalCategories);
|
||||
System.LootedItemsTracker.Enable();
|
||||
|
||||
System.AddonInventoryWindow.DebugOpen();
|
||||
|
||||
@@ -2,6 +2,7 @@ using AetherBags.Addons;
|
||||
using AetherBags.Configuration;
|
||||
using AetherBags.Inventory;
|
||||
using AetherBags.IPC;
|
||||
using AetherBags.IPC.AetherBagsAPI;
|
||||
using AetherBags.Monitoring;
|
||||
|
||||
namespace AetherBags;
|
||||
@@ -13,6 +14,7 @@ public static class System
|
||||
public static AddonRetainerWindow AddonRetainerWindow { get; set; } = null!;
|
||||
public static AddonConfigurationWindow AddonConfigurationWindow { 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 LootedItemsTracker LootedItemsTracker { get; set; } = null!;
|
||||
}
|
||||
+1
-1
Submodule KamiToolKit updated: 811154c8f8...7720ab0741
Reference in New Issue
Block a user