diff --git a/AetherBags/Helpers/RegexCache.cs b/AetherBags/Helpers/RegexCache.cs
new file mode 100644
index 0000000..b6ee376
--- /dev/null
+++ b/AetherBags/Helpers/RegexCache.cs
@@ -0,0 +1,47 @@
+using System.Collections.Concurrent;
+using System.Text.RegularExpressions;
+
+namespace AetherBags.Helpers;
+
+///
+/// Thread-safe cache for compiled Regex objects to avoid repeated compilation overhead.
+///
+internal static class RegexCache
+{
+ private const int MaxCacheSize = 128;
+ private static readonly ConcurrentDictionary Cache = new();
+
+ ///
+ /// Gets or creates a compiled Regex for the given pattern with case-insensitive matching.
+ /// Returns null if the pattern is invalid.
+ ///
+ public static Regex? GetOrCreate(string pattern)
+ {
+ if (string.IsNullOrEmpty(pattern))
+ return null;
+
+ if (Cache.TryGetValue(pattern, out var cached))
+ return cached;
+
+ try
+ {
+ var regex = new Regex(pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ if (Cache.Count < MaxCacheSize)
+ {
+ Cache.TryAdd(pattern, regex);
+ }
+
+ return regex;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ ///
+ /// Clears the regex cache. Call when configuration changes significantly.
+ ///
+ public static void Clear() => Cache.Clear();
+}
diff --git a/AetherBags/Inventory/Categories/CategoryBucket.cs b/AetherBags/Inventory/Categories/CategoryBucket.cs
index 3dda341..b1b9695 100644
--- a/AetherBags/Inventory/Categories/CategoryBucket.cs
+++ b/AetherBags/Inventory/Categories/CategoryBucket.cs
@@ -10,6 +10,7 @@ public sealed class CategoryBucket
public List Items = null!;
public List FilteredItems = null!;
public bool Used;
+ public bool NeedsSorting = true;
}
public sealed class ItemCountDescComparer : IComparer
diff --git a/AetherBags/Inventory/Categories/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs
index b24db77..227593b 100644
--- a/AetherBags/Inventory/Categories/CategoryBucketManager.cs
+++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Linq;
using AetherBags.Configuration;
using AetherBags.Inventory.Items;
using KamiToolKit.Classes;
@@ -49,6 +48,7 @@ public static class CategoryBucketManager
bucket.Used = false;
bucket.Items.Clear();
bucket.FilteredItems.Clear();
+ bucket.NeedsSorting = true;
}
}
@@ -302,8 +302,9 @@ public static class CategoryBucketManager
CategoryInfo miscInfo;
if (itemInfoByKey.Count > 0)
{
- var sample = itemInfoByKey.Values.First();
- miscInfo = GetCategoryInfoCached(0u, sample);
+ using var enumerator = itemInfoByKey.Values.GetEnumerator();
+ enumerator.MoveNext();
+ miscInfo = GetCategoryInfoCached(0u, enumerator.Current);
}
else
{
@@ -353,7 +354,12 @@ public static class CategoryBucketManager
continue;
// TODO: Make configurable
- bucket.Items.Sort(ItemCountDescComparer.Instance);
+ // Only sort if items changed
+ if (bucket.NeedsSorting)
+ {
+ bucket.Items.Sort(ItemCountDescComparer.Instance);
+ bucket.NeedsSorting = false;
+ }
sortedCategoryKeys.Add(bucket.Key);
}
diff --git a/AetherBags/Inventory/Categories/InventoryFilter.cs b/AetherBags/Inventory/Categories/InventoryFilter.cs
index 42d91ca..a35f7f6 100644
--- a/AetherBags/Inventory/Categories/InventoryFilter.cs
+++ b/AetherBags/Inventory/Categories/InventoryFilter.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
+using AetherBags.Helpers;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory.Categories;
@@ -17,17 +18,8 @@ public static class InventoryFilter
if (string.IsNullOrEmpty(filterString))
return allCategories;
- Regex? re = null;
- bool regexValid = true;
-
- try
- {
- re = new Regex(filterString, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
- }
- catch
- {
- regexValid = false;
- }
+ Regex? re = RegexCache.GetOrCreate(filterString);
+ bool regexValid = re != null;
filteredCategories.Clear();
diff --git a/AetherBags/Inventory/Categories/UserCategoryMatcher.cs b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs
index 32edcdb..ea33e63 100644
--- a/AetherBags/Inventory/Categories/UserCategoryMatcher.cs
+++ b/AetherBags/Inventory/Categories/UserCategoryMatcher.cs
@@ -1,6 +1,6 @@
using System;
-using System.Text.RegularExpressions;
using AetherBags.Configuration;
+using AetherBags.Helpers;
using AetherBags.Inventory.Items;
namespace AetherBags.Inventory.Categories;
@@ -30,17 +30,11 @@ internal static class UserCategoryMatcher
if (string.IsNullOrWhiteSpace(pattern))
continue;
- try
+ var regex = RegexCache.GetOrCreate(pattern);
+ if (regex != null && regex.IsMatch(item.Name))
{
- if (Regex.IsMatch(item.Name, pattern, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase))
- {
- matchesAnyIdentification = true;
- break;
- }
- }
- catch
- {
- // Invalid regex: ignore it.
+ matchesAnyIdentification = true;
+ break;
}
}
}
diff --git a/AetherBags/Inventory/Context/HighlightState.cs b/AetherBags/Inventory/Context/HighlightState.cs
index a763d49..00dd134 100644
--- a/AetherBags/Inventory/Context/HighlightState.cs
+++ b/AetherBags/Inventory/Context/HighlightState.cs
@@ -18,13 +18,27 @@ public static class HighlightState
private static readonly Dictionary ids, Vector3 color)> Labels = new();
private static readonly Dictionary> PerItemLabels = new();
+ // Flat cache for O(1) lookups
+ private static readonly Dictionary CachedEntries = new(capacity: 512);
+ private static bool _cacheValid;
+ private static int _version;
+
+ ///
+ /// Version counter that increments when highlight state changes.
+ /// Used by ItemInfo to detect when cached visual state is stale.
+ ///
+ public static int Version => _version;
+
public static string? SelectedAllaganToolsFilterKey { get; set; } = string.Empty;
public static string? SelectedBisBuddyFilterKey { get; set; } = string.Empty;
public static bool IsFilterActive => Filters.Count > 0;
public static void SetFilter(HighlightSource source, IEnumerable ids)
- => Filters[source] = new HashSet(ids);
+ {
+ Filters[source] = new HashSet(ids);
+ _version++;
+ }
public static bool IsInActiveFilters(uint itemId)
{
@@ -36,19 +50,42 @@ public static class HighlightState
public static HighlightEntry? GetHighlightEntry(uint itemId)
{
+ EnsureCacheValid();
+ return CachedEntries.TryGetValue(itemId, out var entry) ? entry : null;
+ }
+
+ private static void EnsureCacheValid()
+ {
+ if (_cacheValid) return;
+
+ CachedEntries.Clear();
+
+ // PerItemLabels have priority - add them first
foreach (var perItemLabel in PerItemLabels.Values)
{
- if (perItemLabel.TryGetValue(itemId, out var entry))
- return entry;
+ foreach (var (id, entry) in perItemLabel)
+ {
+ CachedEntries.TryAdd(id, entry);
+ }
}
+ // Labels are fallback - only add if not already present
foreach (var label in Labels.Values)
{
- if (label.ids.Contains(itemId))
- return new HighlightEntry(itemId, label.color);
+ var color = label.color;
+ foreach (var id in label.ids)
+ {
+ CachedEntries.TryAdd(id, new HighlightEntry(id, color));
+ }
}
- return null;
+ _cacheValid = true;
+ }
+
+ private static void InvalidateCache()
+ {
+ _cacheValid = false;
+ _version++;
}
public static Vector3? GetLabelColor(uint itemId)
@@ -58,6 +95,7 @@ public static class HighlightState
{
PerItemLabels.Remove(source);
Labels[source] = (new HashSet(ids), color);
+ InvalidateCache();
}
public static void SetLabelWithColors(HighlightSource source, Dictionary itemColors)
@@ -76,6 +114,7 @@ public static class HighlightState
}
PerItemLabels[source] = entries;
+ InvalidateCache();
}
public static void SetLabelWithColors(HighlightSource source, IEnumerable entries)
@@ -89,6 +128,7 @@ public static class HighlightState
}
PerItemLabels[source] = dict;
+ InvalidateCache();
}
public static void SetLabelWithColors(HighlightSource source, Dictionary itemColors)
@@ -102,6 +142,7 @@ public static class HighlightState
}
PerItemLabels[source] = entries;
+ InvalidateCache();
}
public static void ClearAll()
@@ -109,14 +150,22 @@ public static class HighlightState
Filters.Clear();
Labels.Clear();
PerItemLabels.Clear();
+ CachedEntries.Clear();
+ _cacheValid = true; // Empty cache is valid
+ _version++;
SelectedAllaganToolsFilterKey = string.Empty;
}
- public static void ClearFilter(HighlightSource source) => Filters.Remove(source);
+ public static void ClearFilter(HighlightSource source)
+ {
+ Filters.Remove(source);
+ _version++;
+ }
public static void ClearLabel(HighlightSource source)
{
Labels.Remove(source);
PerItemLabels.Remove(source);
+ InvalidateCache();
}
}
\ No newline at end of file
diff --git a/AetherBags/Inventory/Items/ItemInfo.cs b/AetherBags/Inventory/Items/ItemInfo.cs
index b4a59fd..1a7d822 100644
--- a/AetherBags/Inventory/Items/ItemInfo.cs
+++ b/AetherBags/Inventory/Items/ItemInfo.cs
@@ -1,6 +1,7 @@
using System;
using System.Numerics;
using System.Text.RegularExpressions;
+using AetherBags.Helpers;
using AetherBags.Inventory.Context;
using FFXIVClientStructs.FFXIV.Client.Game;
using Lumina.Excel;
@@ -23,6 +24,12 @@ public sealed class ItemInfo : IEquatable
private string? _name;
private string? _description;
+ private string? _levelString;
+ private string? _itemLevelString;
+
+ private int _cachedHighlightVersion = -1;
+ private float _cachedVisualAlpha;
+ private Vector3 _cachedHighlightColor;
private ref readonly Item Row
{
@@ -44,6 +51,8 @@ public sealed class ItemInfo : IEquatable
public int Level => Row.LevelEquip;
public int ItemLevel => (int)Row.LevelItem.RowId;
+ private string LevelString => _levelString ??= Level.ToString();
+ private string ItemLevelString => _itemLevelString ??= ItemLevel.ToString();
public int Rarity => Row.Rarity;
public uint VendorPrice => Row.PriceLow;
public uint StackSize => Row.StackSize;
@@ -90,19 +99,37 @@ public sealed class ItemInfo : IEquatable
}
}
- public float VisualAlpha => IsEligibleForContext ? 1.0f : 0.4f;
+ public float VisualAlpha
+ {
+ get
+ {
+ EnsureVisualStateCached();
+ return _cachedVisualAlpha;
+ }
+ }
public Vector3 HighlightOverlayColor
{
get
{
- if (!System.Config.Categories.BisBuddyEnabled)
- return Vector3.Zero;
-
- return HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero;
+ EnsureVisualStateCached();
+ return _cachedHighlightColor;
}
}
+ private void EnsureVisualStateCached()
+ {
+ int currentVersion = HighlightState.Version;
+ if (_cachedHighlightVersion == currentVersion)
+ return;
+
+ _cachedVisualAlpha = IsEligibleForContext ? 1.0f : 0.4f;
+ _cachedHighlightColor = System.Config.Categories.BisBuddyEnabled
+ ? HighlightState.GetLabelColor(Item.ItemId) ?? Vector3.Zero
+ : Vector3.Zero;
+ _cachedHighlightVersion = currentVersion;
+ }
+
private bool CheckNativeContextEligibility()
{
uint contextId = InventoryContextState.ActiveContextId;
@@ -138,14 +165,16 @@ public sealed class ItemInfo : IEquatable
if (string.IsNullOrEmpty(searchTerms))
return true;
- var re = new Regex(searchTerms, RegexOptions.CultureInvariant | RegexOptions.IgnoreCase);
+ var re = RegexCache.GetOrCreate(searchTerms);
+ if (re == null)
+ return false;
if (re.IsMatch(Name)) return true;
if (re.IsMatch(Description)) return true;
- if (re.IsMatch(Level.ToString())) return true;
- if (re.IsMatch(ItemLevel.ToString())) return true;
+ if (re.IsMatch(LevelString)) return true;
+ if (re.IsMatch(ItemLevelString)) return true;
return false;
}
@@ -155,8 +184,8 @@ public sealed class ItemInfo : IEquatable
if (re.IsMatch(Name)) return true;
if (re.IsMatch(Description)) return true;
- if (re.IsMatch(Level.ToString())) return true;
- if (re.IsMatch(ItemLevel.ToString())) return true;
+ if (re.IsMatch(LevelString)) return true;
+ if (re.IsMatch(ItemLevelString)) return true;
return false;
}
diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs
index c4d9cc1..9f0ed23 100644
--- a/AetherBags/Inventory/State/InventoryStateBase.cs
+++ b/AetherBags/Inventory/State/InventoryStateBase.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
-using System.Linq;
using AetherBags.Configuration;
using AetherBags.Currency;
using AetherBags.Inventory.Categories;
@@ -109,7 +108,7 @@ public abstract class InventoryStateBase
}
else
{
- UpdateAllaganHighlight(HighlightState.SelectedBisBuddyFilterKey);
+ UpdateBisBuddyHighlight(HighlightState.SelectedBisBuddyFilterKey);
}
}
else
@@ -156,10 +155,10 @@ public abstract class InventoryStateBase
return;
}
- var filterItems = System.IPC.AllaganTools.GetFilterItems(filterKey);
- if (filterItems != null)
+ var bisItems = System.IPC.BisBuddy.ItemLookup;
+ if (bisItems.Count > 0)
{
- HighlightState.SetFilter(HighlightSource.BiSBuddy, filterItems.Keys);
+ HighlightState.SetFilter(HighlightSource.BiSBuddy, bisItems.Keys);
}
else
{