diff --git a/AetherBags/Configuration/CategorySettings.cs b/AetherBags/Configuration/CategorySettings.cs index 12547d3..9d288b8 100644 --- a/AetherBags/Configuration/CategorySettings.cs +++ b/AetherBags/Configuration/CategorySettings.cs @@ -12,8 +12,9 @@ public class CategorySettings public bool GameCategoriesEnabled { get; set; } = true; public bool UserCategoriesEnabled { get; set; } = true; public bool BisBuddyEnabled { get; set; } = true; + public PluginFilterMode BisBuddyMode { get; set; } = PluginFilterMode.Highlight; public bool AllaganToolsCategoriesEnabled { get; set; } = false; - public AllaganToolsFilterMode AllaganToolsMode { get; set; } = AllaganToolsFilterMode.Highlight; + public PluginFilterMode AllaganToolsFilterMode { get; set; } = PluginFilterMode.Highlight; public List UserCategories { get; set; } = new(); } @@ -81,7 +82,7 @@ public enum ToggleFilterState Disallow = 2, } -public enum AllaganToolsFilterMode +public enum PluginFilterMode { Categorize = 0, Highlight = 1, diff --git a/AetherBags/IPC/BisBuddyIPC.cs b/AetherBags/IPC/BisBuddyIPC.cs index 1317fe4..4d5f5a1 100644 --- a/AetherBags/IPC/BisBuddyIPC.cs +++ b/AetherBags/IPC/BisBuddyIPC.cs @@ -7,15 +7,33 @@ using Dalamud.Plugin.Ipc; namespace AetherBags.IPC; +public record BisItemEntry(uint ItemId, Vector4 Color); + +public record BisItemFilter( + bool IncludePrereqs = true, + bool IncludeMateria = true, + bool IncludeCollected = false, + bool IncludeObtainable = true, + bool IncludeCollectedPrereqs = true +); + public class BisBuddyIPC : IDisposable { private ICallGateSubscriber? _isInitialized; private ICallGateSubscriber? _initialized; - private ICallGateSubscriber>? _getBisItems; - private ICallGateSubscriber, bool>? _bisItemsChanged; + private ICallGateSubscriber>? _getInventoryHighlightItems; + private ICallGateSubscriber, bool>? _inventoryHighlightItemsChanged; + private ICallGateSubscriber>? _getBisItemsFiltered; public bool IsReady { get; private set; } - private static readonly Vector3 BisColor = new(0.0f, 0.3f, 0.0f); + + public List CachedBisItems { get; } = new(); + + public Dictionary ItemLookup { get; } = new(); + + public BisItemFilter? CurrentFilter { get; private set; } + + public event Action? OnItemsRefreshed; public BisBuddyIPC() { @@ -23,57 +41,158 @@ public class BisBuddyIPC : IDisposable { _isInitialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.IsInitialized"); _initialized = Services.PluginInterface.GetIpcSubscriber("BisBuddy.Initialized"); - _getBisItems = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetBisItems"); - _bisItemsChanged = Services.PluginInterface.GetIpcSubscriber, bool>("BisBuddy.BisItemsChanged"); + _getInventoryHighlightItems = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetInventoryHighlightItems"); + _inventoryHighlightItemsChanged = Services.PluginInterface.GetIpcSubscriber, bool>("BisBuddy.InventoryHighlightItemsChanged"); + _getBisItemsFiltered = Services.PluginInterface.GetIpcSubscriber>("BisBuddy.GetBisItemsFiltered"); - _initialized.Subscribe(OnInitialized); - _bisItemsChanged.Subscribe(UpdateHighlights); + _initialized.Subscribe(OnBisBuddyInitialized); + _inventoryHighlightItemsChanged.Subscribe(OnInventoryHighlightItemsChanged); - try { IsReady = _isInitialized.InvokeFunc(); } catch { IsReady = false; } - - if (IsReady) RequestUpdate(); + try + { + IsReady = _isInitialized.InvokeFunc(); + if (IsReady) RefreshItems(); + } + catch + { + IsReady = false; + } } catch (Exception ex) { Services.Logger.DebugOnly($"BisBuddy not available: {ex.Message}"); + IsReady = false; } } - private void OnInitialized(bool ready) + private void OnBisBuddyInitialized(bool ready) { IsReady = ready; - if (ready) RequestUpdate(); - else HighlightState.ClearLabel(HighlightSource.BiSBuddy); + if (ready) + { + Services.Logger.Information("BisBuddy IPC connected"); + RefreshItems(); + } + else + { + ClearHighlights(); + } } - public void RequestUpdate() + private void OnInventoryHighlightItemsChanged(List items) + { + if (CurrentFilter == null) + { + UpdateCacheAndHighlights(items); + } + } + + public void RefreshItems() { if (!IsReady) return; + try { - var items = _getBisItems?.InvokeFunc(); - if (items != null) UpdateHighlights(items); + List? items; + + if (CurrentFilter != null) + { + items = _getBisItemsFiltered?.InvokeFunc(CurrentFilter); + } + else + { + items = _getInventoryHighlightItems?.InvokeFunc(); + } + + if (items != null) + { + UpdateCacheAndHighlights(items); + } + } + catch (Exception ex) + { + Services.Logger.Warning($"Failed to refresh BisBuddy items: {ex.Message}"); + IsReady = false; } - catch { IsReady = false; } } - private void UpdateHighlights(List? itemIds) + public void SetFilter(BisItemFilter? filter) { - if (!System.Config.Categories.BisBuddyEnabled || itemIds == null || itemIds.Count == 0) + CurrentFilter = filter; + RefreshItems(); + } + + public void ShowAllItems() + { + SetFilter(new BisItemFilter(IncludeCollected: true)); + } + + public void ShowUncollectedOnly() + { + SetFilter(new BisItemFilter(IncludeCollected: false)); + } + + public void UseInventoryConfig() + { + SetFilter(null); + } + + private void UpdateCacheAndHighlights(List items) + { + CachedBisItems.Clear(); + ItemLookup.Clear(); + + foreach (var item in items) + { + CachedBisItems.Add(item); + ItemLookup[item.ItemId] = item; + } + + Services.Logger.DebugOnly($"Refreshed {CachedBisItems.Count} BisBuddy items"); + + ApplyHighlights(); + OnItemsRefreshed?.Invoke(); + } + + private void ApplyHighlights() + { + if (!System.Config.Categories.BisBuddyEnabled || CachedBisItems.Count == 0) { HighlightState.ClearLabel(HighlightSource.BiSBuddy); } else { - HighlightState.SetLabel(HighlightSource.BiSBuddy, itemIds, BisColor); + var highlights = new Dictionary(CachedBisItems.Count); + foreach (var item in CachedBisItems) + { + highlights[item.ItemId] = item.Color; + } + HighlightState.SetLabelWithColors(HighlightSource.BiSBuddy, highlights); } InventoryOrchestrator.RefreshHighlights(); } + private void ClearHighlights() + { + CachedBisItems.Clear(); + ItemLookup.Clear(); + HighlightState.ClearLabel(HighlightSource.BiSBuddy); + InventoryOrchestrator.RefreshHighlights(); + } + + public bool IsBisItem(uint itemId) + => ItemLookup.ContainsKey(itemId); + + public BisItemEntry? GetBisItem(uint itemId) + => ItemLookup.GetValueOrDefault(itemId); + + public Vector4? GetItemColor(uint itemId) + => GetBisItem(itemId)?.Color; + public void Dispose() { - _initialized?.Unsubscribe(OnInitialized); - _bisItemsChanged?.Unsubscribe(UpdateHighlights); + _initialized?.Unsubscribe(OnBisBuddyInitialized); + _inventoryHighlightItemsChanged?.Unsubscribe(OnInventoryHighlightItemsChanged); } } \ No newline at end of file diff --git a/AetherBags/Inventory/Categories/CategoryBucketManager.cs b/AetherBags/Inventory/Categories/CategoryBucketManager.cs index 0ca3e4d..5a7c1e8 100644 --- a/AetherBags/Inventory/Categories/CategoryBucketManager.cs +++ b/AetherBags/Inventory/Categories/CategoryBucketManager.cs @@ -21,9 +21,19 @@ public static class CategoryBucketManager private const uint AllaganFilterKeyFlag = 0x4000_0000; + private const uint BisBuddyKeyFlag = 0x2000_0000; + public static uint MakeAllaganFilterKey(int index) => AllaganFilterKeyFlag | (uint)(index & 0x3FFF_FFFF); + public static uint MakeBisBuddyKey() + => BisBuddyKeyFlag; + + public static bool IsBisBuddyKey(uint key) + => (key & BisBuddyKeyFlag) != 0 + && (key & AllaganFilterKeyFlag) == 0 + && (key & UserCategoryKeyFlag) == 0; + public static bool IsAllaganFilterKey(uint key) => (key & AllaganFilterKeyFlag) != 0 && (key & UserCategoryKeyFlag) == 0; @@ -219,13 +229,68 @@ public static class CategoryBucketManager } } - if (bucket.Items. Count == 0) + if (bucket.Items.Count == 0) bucket.Used = false; index++; } } + public static void BucketByBisBuddyItems( + Dictionary itemInfoByKey, + Dictionary bucketsByKey, + HashSet claimedKeys, + bool bisCategoriesEnabled) + { + if (!bisCategoriesEnabled) return; + if (!System.IPC.BisBuddy.IsReady) return; + + var bisItems = System.IPC.BisBuddy.ItemLookup; + if (bisItems. Count == 0) return; + + uint bucketKey = MakeBisBuddyKey(); + + if (!bucketsByKey.TryGetValue(bucketKey, out CategoryBucket? bucket)) + { + bucket = new CategoryBucket + { + Key = bucketKey, + Category = new CategoryInfo + { + Name = "[BiS] Best in Slot", + Description = "Items needed for your BiS gearsets", + Color = ColorHelper.GetColor(50), + }, + Items = new List(capacity: 16), + FilteredItems = new List(capacity: 16), + Used = true, + }; + bucketsByKey. Add(bucketKey, bucket); + } + else + { + bucket.Used = true; + } + + foreach (var itemKvp in itemInfoByKey) + { + ulong itemKey = itemKvp.Key; + ItemInfo item = itemKvp.Value; + + if (claimedKeys.Contains(itemKey)) + continue; + + if (bisItems.ContainsKey(item.Item.ItemId)) + { + bucket.Items. Add(item); + claimedKeys.Add(itemKey); + } + } + + if (bucket.Items.Count == 0) + bucket.Used = false; + } + public static void BucketUnclaimedToMisc( Dictionary itemInfoByKey, Dictionary bucketsByKey, diff --git a/AetherBags/Inventory/Context/HighlightState.cs b/AetherBags/Inventory/Context/HighlightState.cs index e278e5f..a763d49 100644 --- a/AetherBags/Inventory/Context/HighlightState.cs +++ b/AetherBags/Inventory/Context/HighlightState.cs @@ -10,12 +10,16 @@ public enum HighlightSource BiSBuddy, } +public record HighlightEntry(uint ItemId, Vector3 Color); + public static class HighlightState { private static readonly Dictionary> Filters = new(); private static readonly Dictionary ids, Vector3 color)> Labels = new(); + private static readonly Dictionary> PerItemLabels = new(); public static string? SelectedAllaganToolsFilterKey { get; set; } = string.Empty; + public static string? SelectedBisBuddyFilterKey { get; set; } = string.Empty; public static bool IsFilterActive => Filters.Count > 0; @@ -30,24 +34,89 @@ public static class HighlightState return false; } - public static Vector3? GetLabelColor(uint itemId) + public static HighlightEntry? GetHighlightEntry(uint itemId) { + foreach (var perItemLabel in PerItemLabels.Values) + { + if (perItemLabel.TryGetValue(itemId, out var entry)) + return entry; + } + foreach (var label in Labels.Values) - if (label.ids.Contains(itemId)) return label.color; + { + if (label.ids.Contains(itemId)) + return new HighlightEntry(itemId, label.color); + } + return null; } - public static void SetLabel(HighlightSource source, IEnumerable ids, Vector3 color) - => Labels[source] = (new HashSet(ids), color); + public static Vector3? GetLabelColor(uint itemId) + => GetHighlightEntry(itemId)?.Color; + public static void SetLabel(HighlightSource source, IEnumerable ids, Vector3 color) + { + PerItemLabels.Remove(source); + Labels[source] = (new HashSet(ids), color); + } + + public static void SetLabelWithColors(HighlightSource source, Dictionary itemColors) + { + Labels.Remove(source); + + var entries = new Dictionary(itemColors.Count); + foreach (var (itemId, color) in itemColors) + { + var rgb = new Vector3( + color.X * color.W, + color.Y * color.W, + color.Z * color.W + ); + entries[itemId] = new HighlightEntry(itemId, rgb); + } + + PerItemLabels[source] = entries; + } + + public static void SetLabelWithColors(HighlightSource source, IEnumerable entries) + { + Labels.Remove(source); + + var dict = new Dictionary(); + foreach (var entry in entries) + { + dict[entry.ItemId] = entry; + } + + PerItemLabels[source] = dict; + } + + public static void SetLabelWithColors(HighlightSource source, Dictionary itemColors) + { + Labels.Remove(source); + + var entries = new Dictionary(itemColors.Count); + foreach (var (itemId, color) in itemColors) + { + entries[itemId] = new HighlightEntry(itemId, color); + } + + PerItemLabels[source] = entries; + } public static void ClearAll() { Filters.Clear(); Labels.Clear(); + PerItemLabels.Clear(); SelectedAllaganToolsFilterKey = string.Empty; } public static void ClearFilter(HighlightSource source) => Filters.Remove(source); - public static void ClearLabel(HighlightSource source) => Labels.Remove(source); + + public static void ClearLabel(HighlightSource source) + { + Labels.Remove(source); + PerItemLabels.Remove(source); + } } \ No newline at end of file diff --git a/AetherBags/Inventory/State/InventoryStateBase.cs b/AetherBags/Inventory/State/InventoryStateBase.cs index 1d8a1af..34c058b 100644 --- a/AetherBags/Inventory/State/InventoryStateBase.cs +++ b/AetherBags/Inventory/State/InventoryStateBase.cs @@ -66,6 +66,7 @@ public abstract class InventoryStateBase bool userCategoriesEnabled = config.Categories.UserCategoriesEnabled && categoriesEnabled; bool gameCategoriesEnabled = config.Categories.GameCategoriesEnabled && categoriesEnabled; bool allaganCategoriesEnabled = config.Categories.AllaganToolsCategoriesEnabled && categoriesEnabled; + bool bisCategoriesEnabled = config.Categories.BisBuddyEnabled && categoriesEnabled; // TODO: Cache this when config changes var userCategories = config.Categories.UserCategories.Where(c => c.Enabled).ToList(); @@ -77,7 +78,7 @@ public abstract class InventoryStateBase if (allaganCategoriesEnabled) { - if (config.Categories.AllaganToolsMode == AllaganToolsFilterMode.Categorize) + if (config.Categories.AllaganToolsFilterMode == PluginFilterMode.Categorize) { CategoryBucketManager.BucketByAllaganFilters(ItemInfoByKey, BucketsByKey, ClaimedKeys, true); HighlightState.ClearFilter(HighlightSource.AllaganTools); @@ -92,6 +93,23 @@ public abstract class InventoryStateBase HighlightState.ClearFilter(HighlightSource.AllaganTools); } + if (bisCategoriesEnabled) + { + if (config.Categories.BisBuddyMode == PluginFilterMode.Categorize) + { + CategoryBucketManager.BucketByAllaganFilters(ItemInfoByKey, BucketsByKey, ClaimedKeys, true); + HighlightState.ClearFilter(HighlightSource.AllaganTools); + } + else + { + UpdateAllaganHighlight(HighlightState.SelectedBisBuddyFilterKey); + } + } + else + { + HighlightState.ClearFilter(HighlightSource.BiSBuddy); + } + if (gameCategoriesEnabled) { CategoryBucketManager.BucketByGameCategories( diff --git a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs index 4752f5f..f202acb 100644 --- a/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs +++ b/AetherBags/Nodes/Configuration/Category/CategoryGeneralConfigurationNode.cs @@ -80,6 +80,27 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode bool bisBuddyReady = System.IPC.BisBuddy?.IsReady ?? false; + LabeledDropdownNode? bbModeDropdown = new LabeledDropdownNode + { + Size = new Vector2(300, 20), + LabelText = "Filter Display Mode", + LabelTextFlags = TextFlags.AutoAdjustNodeSize, + IsEnabled = config.BisBuddyEnabled && bisBuddyReady, + Options = Enum.GetNames(typeof(PluginFilterMode)).ToList(), + SelectedOption = config.BisBuddyMode.ToString(), + OnOptionSelected = selected => + { + if (Enum.TryParse(selected, out var parsed)) + { + config.BisBuddyMode = parsed; + if (parsed == PluginFilterMode.Categorize) + HighlightState.ClearFilter(HighlightSource.AllaganTools); + + RefreshInventory(); + } + } + }; + CheckboxNode bisBuddyEnabled = new CheckboxNode { Size = Size with { Y = 18 }, @@ -90,11 +111,13 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode OnClick = isChecked => { config.BisBuddyEnabled = isChecked; - System.IPC.BisBuddy?.RequestUpdate(); + if (bbModeDropdown != null) bbModeDropdown.IsEnabled = isChecked; + if (isChecked) System.IPC.BisBuddy?.RefreshItems(); RefreshInventory(); } }; AddNode(bisBuddyEnabled); + AddNode(bbModeDropdown); bool allaganReady = System.IPC.AllaganTools?.IsReady ?? false; @@ -104,14 +127,14 @@ public sealed class CategoryGeneralConfigurationNode : TabbedVerticalListNode LabelText = "Filter Display Mode", LabelTextFlags = TextFlags.AutoAdjustNodeSize, IsEnabled = config.AllaganToolsCategoriesEnabled && allaganReady, - Options = Enum.GetNames(typeof(AllaganToolsFilterMode)).ToList(), - SelectedOption = config.AllaganToolsMode.ToString(), + Options = Enum.GetNames(typeof(PluginFilterMode)).ToList(), + SelectedOption = config.AllaganToolsFilterMode.ToString(), OnOptionSelected = selected => { - if (Enum.TryParse(selected, out var parsed)) + if (Enum.TryParse(selected, out var parsed)) { - config.AllaganToolsMode = parsed; - if (parsed == AllaganToolsFilterMode.Categorize) + config.AllaganToolsFilterMode = parsed; + if (parsed == PluginFilterMode.Categorize) HighlightState.ClearFilter(HighlightSource.AllaganTools); RefreshInventory();