Change BisBuddy IPC

This commit is contained in:
Zeffuro
2026-01-07 21:58:01 +01:00
parent 7fdb4fa43e
commit 9a188514d8
6 changed files with 332 additions and 37 deletions
+3 -2
View File
@@ -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<UserCategoryDefinition> UserCategories { get; set; } = new();
}
@@ -81,7 +82,7 @@ public enum ToggleFilterState
Disallow = 2,
}
public enum AllaganToolsFilterMode
public enum PluginFilterMode
{
Categorize = 0,
Highlight = 1,
+141 -22
View File
@@ -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<bool>? _isInitialized;
private ICallGateSubscriber<bool, bool>? _initialized;
private ICallGateSubscriber<List<uint>>? _getBisItems;
private ICallGateSubscriber<List<uint>, bool>? _bisItemsChanged;
private ICallGateSubscriber<List<BisItemEntry>>? _getInventoryHighlightItems;
private ICallGateSubscriber<List<BisItemEntry>, bool>? _inventoryHighlightItemsChanged;
private ICallGateSubscriber<BisItemFilter, List<BisItemEntry>>? _getBisItemsFiltered;
public bool IsReady { get; private set; }
private static readonly Vector3 BisColor = new(0.0f, 0.3f, 0.0f);
public List<BisItemEntry> CachedBisItems { get; } = new();
public Dictionary<uint, BisItemEntry> 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<bool>("BisBuddy.IsInitialized");
_initialized = Services.PluginInterface.GetIpcSubscriber<bool, bool>("BisBuddy.Initialized");
_getBisItems = Services.PluginInterface.GetIpcSubscriber<List<uint>>("BisBuddy.GetBisItems");
_bisItemsChanged = Services.PluginInterface.GetIpcSubscriber<List<uint>, bool>("BisBuddy.BisItemsChanged");
_getInventoryHighlightItems = Services.PluginInterface.GetIpcSubscriber<List<BisItemEntry>>("BisBuddy.GetInventoryHighlightItems");
_inventoryHighlightItemsChanged = Services.PluginInterface.GetIpcSubscriber<List<BisItemEntry>, bool>("BisBuddy.InventoryHighlightItemsChanged");
_getBisItemsFiltered = Services.PluginInterface.GetIpcSubscriber<BisItemFilter, List<BisItemEntry>>("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<BisItemEntry> items)
{
if (CurrentFilter == null)
{
UpdateCacheAndHighlights(items);
}
}
public void RefreshItems()
{
if (!IsReady) return;
try
{
var items = _getBisItems?.InvokeFunc();
if (items != null) UpdateHighlights(items);
List<BisItemEntry>? 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<uint>? 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<BisItemEntry> 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<uint, Vector4>(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);
}
}
@@ -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<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey,
HashSet<ulong> 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<ItemInfo>(capacity: 16),
FilteredItems = new List<ItemInfo>(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<ulong, ItemInfo> itemInfoByKey,
Dictionary<uint, CategoryBucket> bucketsByKey,
+74 -5
View File
@@ -10,12 +10,16 @@ public enum HighlightSource
BiSBuddy,
}
public record HighlightEntry(uint ItemId, Vector3 Color);
public static class HighlightState
{
private static readonly Dictionary<HighlightSource, HashSet<uint>> Filters = new();
private static readonly Dictionary<HighlightSource, (HashSet<uint> ids, Vector3 color)> Labels = new();
private static readonly Dictionary<HighlightSource, Dictionary<uint, HighlightEntry>> 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<uint> ids, Vector3 color)
=> Labels[source] = (new HashSet<uint>(ids), color);
public static Vector3? GetLabelColor(uint itemId)
=> GetHighlightEntry(itemId)?.Color;
public static void SetLabel(HighlightSource source, IEnumerable<uint> ids, Vector3 color)
{
PerItemLabels.Remove(source);
Labels[source] = (new HashSet<uint>(ids), color);
}
public static void SetLabelWithColors(HighlightSource source, Dictionary<uint, Vector4> itemColors)
{
Labels.Remove(source);
var entries = new Dictionary<uint, HighlightEntry>(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<HighlightEntry> entries)
{
Labels.Remove(source);
var dict = new Dictionary<uint, HighlightEntry>();
foreach (var entry in entries)
{
dict[entry.ItemId] = entry;
}
PerItemLabels[source] = dict;
}
public static void SetLabelWithColors(HighlightSource source, Dictionary<uint, Vector3> itemColors)
{
Labels.Remove(source);
var entries = new Dictionary<uint, HighlightEntry>(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);
}
}
@@ -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(
@@ -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<PluginFilterMode>(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<AllaganToolsFilterMode>(selected, out var parsed))
if (Enum.TryParse<PluginFilterMode>(selected, out var parsed))
{
config.AllaganToolsMode = parsed;
if (parsed == AllaganToolsFilterMode.Categorize)
config.AllaganToolsFilterMode = parsed;
if (parsed == PluginFilterMode.Categorize)
HighlightState.ClearFilter(HighlightSource.AllaganTools);
RefreshInventory();