From 2025f4e0d23521451a16f238f50b0bc0580b09df Mon Sep 17 00:00:00 2001 From: Administrator Date: Sun, 4 Jan 2026 01:21:22 +0200 Subject: [PATCH] yes Add support for glasses as a bonus equipment slot - Introduce ItemCategory.Glasses and add a "Glasses" tab to the UI - Load and display glasses items with filtering and virtual scrolling - Allow users to preview, select, and apply glasses to their character - Add interop for setting bonus items (glasses) via Glamourer API - Update Apply Gear Set logic and icon handling for glasses - Improve filter caching to support per-category filters --- GlamourBrowser/DataCache.cs | 5 + GlamourBrowser/Interop/Glamourer.cs | 13 ++ GlamourBrowser/PluginUi.cs | 2 +- GlamourBrowser/Ui/MainInterface.cs | 215 +++++++++++++++++++++++++++- 4 files changed, 227 insertions(+), 8 deletions(-) diff --git a/GlamourBrowser/DataCache.cs b/GlamourBrowser/DataCache.cs index d4c2da7..1389e93 100644 --- a/GlamourBrowser/DataCache.cs +++ b/GlamourBrowser/DataCache.cs @@ -13,6 +13,11 @@ namespace GlamourBrowser { row.EquipSlotCategory.Value!.SoulCrystal == 0) .ToImmutableList()); + public static Lazy> GlassesItems { get; } = + new(() => Service.DataManager.GetExcelSheet(ClientLanguage.English)! + .Where(row => row.Name.ByteLength > 0) + .ToImmutableList()); + public static Lazy> StainLookup { get; } = new (() => Service.DataManager.GetExcelSheet(ClientLanguage.English)! diff --git a/GlamourBrowser/Interop/Glamourer.cs b/GlamourBrowser/Interop/Glamourer.cs index 406d6d2..14b428c 100644 --- a/GlamourBrowser/Interop/Glamourer.cs +++ b/GlamourBrowser/Interop/Glamourer.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; namespace GlamourBrowser.Interop { internal class Glamourer { private static SetItem _SetItem { get; set; } = null!; + private static SetBonusItem _SetBonusItem { get; set; } = null!; private static GetState _GetState { get; set; } = null!; private static RevertState _RevertState { get; set; } = null!; @@ -64,11 +65,23 @@ namespace GlamourBrowser.Interop { }; } + public static void SetBonusItem(int playerIndex, ApiBonusSlot slot, uint itemId) { + if (!IsAvailable()) + return; + + try { + Service.Framework.Run(() => { + _SetBonusItem.Invoke(playerIndex, slot, itemId); + }); + } catch (Exception) { } + } + public static void Initialize(IDalamudPluginInterface pluginInterface) { if (Initialized) return; _SetItem = new SetItem(pluginInterface); + _SetBonusItem = new SetBonusItem(pluginInterface); _GetState = new GetState(pluginInterface); _RevertState = new RevertState(pluginInterface); diff --git a/GlamourBrowser/PluginUi.cs b/GlamourBrowser/PluginUi.cs index e2c117e..bdb8f54 100644 --- a/GlamourBrowser/PluginUi.cs +++ b/GlamourBrowser/PluginUi.cs @@ -31,7 +31,7 @@ namespace GlamourBrowser { this.MainInterface.Toggle(); } - internal IDalamudTextureWrap? GetIcon(ushort id) { + internal IDalamudTextureWrap? GetIcon(uint id) { var icon = Service.TextureProvider.GetFromGameIcon(new Dalamud.Interface.Textures.GameIconLookup(id)).GetWrapOrDefault(); return icon; } diff --git a/GlamourBrowser/Ui/MainInterface.cs b/GlamourBrowser/Ui/MainInterface.cs index 4dd9136..9551975 100644 --- a/GlamourBrowser/Ui/MainInterface.cs +++ b/GlamourBrowser/Ui/MainInterface.cs @@ -1,5 +1,5 @@ using Dalamud.Bindings.ImGui; - +using Glamourer.Api.Enums; using Lumina.Excel.Sheets; using System; using System.Collections.Generic; @@ -22,6 +22,7 @@ namespace GlamourBrowser.Ui { Gloves, Legs, Feet, + Glasses, } private PluginUi Ui { get; } @@ -31,12 +32,13 @@ namespace GlamourBrowser.Ui { private Dictionary ScrollPositions { get; set; } = new(); private Dictionary SelectedStain1 { get; set; } = new(); private Dictionary SelectedStain2 { get; set; } = new(); + private Glasses? SelectedGlasses { get; set; } = null; private bool _visible; private string _itemFilter = string.Empty; private ItemCategory _currentCategory = ItemCategory.Head; private ItemCategory _previousCategory = ItemCategory.Head; - private string _lastFilterUsed = string.Empty; + private Dictionary _lastFilterUsed = new(); private string _dyeFilter = string.Empty; internal MainInterface(PluginUi ui) { @@ -126,18 +128,28 @@ namespace GlamourBrowser.Ui { } private List GetFilteredItems() { + // Glasses are handled separately, so return empty list if on Glasses tab + if (_currentCategory == ItemCategory.Glasses) { + return new List(); + } + var filter = this._itemFilter.ToLowerInvariant(); - - if (!FilteredItemsCache.ContainsKey(_currentCategory) || _lastFilterUsed != _itemFilter) { + + // Check if we need to recalculate the filter for this category + bool needsRecalculation = !FilteredItemsCache.ContainsKey(_currentCategory) || + !_lastFilterUsed.ContainsKey(_currentCategory) || + _lastFilterUsed[_currentCategory] != _itemFilter; + + if (needsRecalculation) { var items = ItemsByCategory[_currentCategory]; var filtered = string.IsNullOrEmpty(this._itemFilter) ? items.ToList() : items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList(); - + FilteredItemsCache[_currentCategory] = filtered; - _lastFilterUsed = _itemFilter; + _lastFilterUsed[_currentCategory] = _itemFilter; } - + return FilteredItemsCache[_currentCategory]; } @@ -296,6 +308,9 @@ namespace GlamourBrowser.Ui { ImGui.Spacing(); } + // Draw glasses/bonus slot + DrawGlassesSlot(); + ImGui.Separator(); ImGui.Spacing(); @@ -308,6 +323,64 @@ namespace GlamourBrowser.Ui { } } + private unsafe void DrawGlassesSlot() { + ImGui.TextUnformatted("Glasses"); + + ImGui.SameLine(); + ImGui.PushStyleVar(ImGuiStyleVar.FramePadding, new Vector2(2, 2)); + if (ImGui.Button("?##info-Glasses", new Vector2(20, 20))) { + if (SelectedGlasses.HasValue) { + Interop.AT.OpenMoreInformationSub(SelectedGlasses.Value.RowId.ToString()); + } + } + ImGui.PopStyleVar(); + + if (ImGui.IsItemHovered()) { + ImGui.SetTooltip("Click to see item location"); + } + + var drawCursor = ImGui.GetCursorScreenPos(); + var slotSize = SelectedGearIconSize + SelectedGearPaddingSize; + var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border); + + ImGui.GetWindowDrawList().AddRect( + drawCursor, + drawCursor + new Vector2(slotSize), + ImGui.ColorConvertFloat4ToU32(borderColour) + ); + + var cursorBefore = ImGui.GetCursorPos(); + ImGui.InvisibleButton("gear-slot Glasses", new Vector2(slotSize)); + var cursorAfter = ImGui.GetCursorPos(); + + if (ImGui.IsItemClicked(ImGuiMouseButton.Right)) { + SelectedGlasses = null; + // Remove glasses from character (item ID 0 means remove/nothing) + Interop.Glamourer.SetBonusItem(0, ApiBonusSlot.Glasses, 0); + } + + if (SelectedGlasses != null) { + try { + var icon = this.Ui.GetIcon((uint)SelectedGlasses.Value.Icon); + if (icon != null) { + ImGui.SetCursorPos(cursorBefore + new Vector2(SelectedGearPaddingSize / 2f)); + ImGui.Image(icon.Handle, new Vector2(SelectedGearIconSize)); + ImGui.SetCursorPos(cursorAfter); + } + } catch { + // Icon not found, skip rendering it + ImGui.SetCursorPos(cursorAfter); + } + + ImGui.TextUnformatted(SelectedGlasses.Value.Singular.ExtractText()); + } else { + ImGui.SetCursorPos(cursorAfter); + ImGui.TextUnformatted("(empty)"); + } + + ImGui.Spacing(); + } + private unsafe bool DrawDyeCombo(string id, Dictionary stainDict, ItemCategory category, bool isPrimary) { var allStains = DataCache.AllStains.Value; var currentStain = stainDict[category]; @@ -471,6 +544,11 @@ namespace GlamourBrowser.Ui { } } + // Apply glasses if selected + if (SelectedGlasses.HasValue) { + Interop.Glamourer.SetBonusItem(playerIndex, Glamourer.Api.Enums.ApiBonusSlot.Glasses, SelectedGlasses.Value.RowId); + } + Service.ChatGui.Print("[Glamour Browser] Gear applied to character!"); } catch (Exception ex) { Service.Log.Error(ex, "Failed to apply gear to character"); @@ -485,6 +563,7 @@ namespace GlamourBrowser.Ui { DrawTabButton("Gloves", ItemCategory.Gloves); DrawTabButton("Bottom", ItemCategory.Legs); DrawTabButton("Shoes", ItemCategory.Feet); + DrawTabButton("Glasses", ItemCategory.Glasses); ImGui.EndTabBar(); } @@ -508,7 +587,129 @@ namespace GlamourBrowser.Ui { } } + private void DrawGlassesGrid() { + var glassesItems = DataCache.GlassesItems.Value; + var filteredGlasses = string.IsNullOrEmpty(this._itemFilter) + ? glassesItems + : glassesItems.Where(item => item.Singular.ExtractText().ToLowerInvariant().Contains(this._itemFilter.ToLowerInvariant())); + + var glassesList = filteredGlasses.ToList(); + + if (glassesList.Count == 0) { + ImGui.TextDisabled("No glasses found"); + return; + } + + // Reset scroll if category changed + if (_currentCategory != _previousCategory) { + ImGui.SetScrollY(0); + _previousCategory = _currentCategory; + } + + if (!ImGui.BeginTable("glasses grid", ItemsPerRow, ImGuiTableFlags.SizingFixedFit | ImGuiTableFlags.NoKeepColumnsVisible)) { + return; + } + + // Virtual scrolling + var scrollY = ImGui.GetScrollY(); + var visibleHeight = ImGui.GetContentRegionAvail().Y; + + var totalRows = (glassesList.Count + ItemsPerRow - 1) / ItemsPerRow; + var firstVisibleRow = Math.Max(0, (int)(scrollY / ItemHeight)); + var lastVisibleRow = Math.Min(totalRows, firstVisibleRow + (int)((visibleHeight / ItemHeight) + 2)); + + if (firstVisibleRow > 0) { + ImGui.TableNextRow(ImGuiTableRowFlags.None, ItemHeight * firstVisibleRow); + } + + for (int row = firstVisibleRow; row < lastVisibleRow; row++) { + ImGui.TableNextRow(); + + for (int col = 0; col < ItemsPerRow; col++) { + var itemIndex = row * ItemsPerRow + col; + ImGui.TableNextColumn(); + + if (itemIndex < glassesList.Count) { + this.DrawGlassesIcon(glassesList[itemIndex]); + } + } + } + + ImGui.EndTable(); + + ImGui.Spacing(); + ImGui.TextDisabled($"Showing {glassesList.Count} glasses"); + } + + private unsafe void DrawGlassesIcon(Glasses glasses) { + var drawCursor = ImGui.GetCursorScreenPos(); + var iconSize = IconSize; + var paddingSize = PaddingSize; + + ImGui.BeginGroup(); + + var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border); + ImGui.GetWindowDrawList().AddRect( + drawCursor, + drawCursor + new Vector2(iconSize + paddingSize), + ImGui.ColorConvertFloat4ToU32(borderColour) + ); + + var cursorBefore = ImGui.GetCursorPos(); + ImGui.InvisibleButton($"glasses {glasses.RowId}", new Vector2(iconSize + paddingSize)); + var cursorAfter = ImGui.GetCursorPos(); + + try { + var icon = this.Ui.GetIcon((uint)glasses.Icon); + if (icon != null) { + ImGui.SetCursorPos(cursorBefore + new Vector2(paddingSize / 2f)); + ImGui.Image(icon.Handle, new Vector2(iconSize)); + ImGui.SetCursorPos(cursorAfter); + } + } catch { + // Icon not found, skip rendering it + ImGui.SetCursorPos(cursorAfter); + } + + ImGui.EndGroup(); + + if (ImGui.IsItemHovered()) { + ImGui.BeginTooltip(); + ImGui.TextUnformatted($"ID: {glasses.RowId}"); + ImGui.TextUnformatted($"Name: {glasses.Singular.ExtractText()}"); + ImGui.EndTooltip(); + } + + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) { + SelectedGlasses = glasses; + this.ApplyGlassesToCharacter(glasses); + } + } + + private void ApplyGlassesToCharacter(Glasses glasses) { + if (Service.ObjectTable.LocalPlayer == null) { + return; + } + + if (!Interop.Glamourer.IsAvailable()) { + return; + } + + try { + int playerIndex = Service.ObjectTable.LocalPlayer.ObjectIndex; + Interop.Glamourer.SetBonusItem(playerIndex, Glamourer.Api.Enums.ApiBonusSlot.Glasses, glasses.RowId); + } catch (Exception ex) { + Service.Log.Error(ex, "Failed to apply glasses to character"); + } + } + private void DrawItemGrid() { + // Handle glasses separately + if (_currentCategory == ItemCategory.Glasses) { + DrawGlassesGrid(); + return; + } + var filteredItems = GetFilteredItems(); if (filteredItems.Count == 0) {