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 {