1 Commits

Author SHA1 Message Date
2025f4e0d2 yes
All checks were successful
Build and Release / Build (push) Successful in 30s
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
2026-01-04 01:21:22 +02:00
4 changed files with 227 additions and 8 deletions

View File

@@ -13,6 +13,11 @@ namespace GlamourBrowser {
row.EquipSlotCategory.Value!.SoulCrystal == 0) row.EquipSlotCategory.Value!.SoulCrystal == 0)
.ToImmutableList()); .ToImmutableList());
public static Lazy<ImmutableList<Glasses>> GlassesItems { get; } =
new(() => Service.DataManager.GetExcelSheet<Glasses>(ClientLanguage.English)!
.Where(row => row.Name.ByteLength > 0)
.ToImmutableList());
public static Lazy<ImmutableDictionary<string, byte>> StainLookup { get; } = public static Lazy<ImmutableDictionary<string, byte>> StainLookup { get; } =
new (() => new (() =>
Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)! Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
namespace GlamourBrowser.Interop { namespace GlamourBrowser.Interop {
internal class Glamourer { internal class Glamourer {
private static SetItem _SetItem { get; set; } = null!; private static SetItem _SetItem { get; set; } = null!;
private static SetBonusItem _SetBonusItem { get; set; } = null!;
private static GetState _GetState { get; set; } = null!; private static GetState _GetState { get; set; } = null!;
private static RevertState _RevertState { 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) { public static void Initialize(IDalamudPluginInterface pluginInterface) {
if (Initialized) if (Initialized)
return; return;
_SetItem = new SetItem(pluginInterface); _SetItem = new SetItem(pluginInterface);
_SetBonusItem = new SetBonusItem(pluginInterface);
_GetState = new GetState(pluginInterface); _GetState = new GetState(pluginInterface);
_RevertState = new RevertState(pluginInterface); _RevertState = new RevertState(pluginInterface);

View File

@@ -31,7 +31,7 @@ namespace GlamourBrowser {
this.MainInterface.Toggle(); 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(); var icon = Service.TextureProvider.GetFromGameIcon(new Dalamud.Interface.Textures.GameIconLookup(id)).GetWrapOrDefault();
return icon; return icon;
} }

View File

@@ -1,5 +1,5 @@
using Dalamud.Bindings.ImGui; using Dalamud.Bindings.ImGui;
using Glamourer.Api.Enums;
using Lumina.Excel.Sheets; using Lumina.Excel.Sheets;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@@ -22,6 +22,7 @@ namespace GlamourBrowser.Ui {
Gloves, Gloves,
Legs, Legs,
Feet, Feet,
Glasses,
} }
private PluginUi Ui { get; } private PluginUi Ui { get; }
@@ -31,12 +32,13 @@ namespace GlamourBrowser.Ui {
private Dictionary<ItemCategory, float> ScrollPositions { get; set; } = new(); private Dictionary<ItemCategory, float> ScrollPositions { get; set; } = new();
private Dictionary<ItemCategory, byte> SelectedStain1 { get; set; } = new(); private Dictionary<ItemCategory, byte> SelectedStain1 { get; set; } = new();
private Dictionary<ItemCategory, byte> SelectedStain2 { get; set; } = new(); private Dictionary<ItemCategory, byte> SelectedStain2 { get; set; } = new();
private Glasses? SelectedGlasses { get; set; } = null;
private bool _visible; private bool _visible;
private string _itemFilter = string.Empty; private string _itemFilter = string.Empty;
private ItemCategory _currentCategory = ItemCategory.Head; private ItemCategory _currentCategory = ItemCategory.Head;
private ItemCategory _previousCategory = ItemCategory.Head; private ItemCategory _previousCategory = ItemCategory.Head;
private string _lastFilterUsed = string.Empty; private Dictionary<ItemCategory, string> _lastFilterUsed = new();
private string _dyeFilter = string.Empty; private string _dyeFilter = string.Empty;
internal MainInterface(PluginUi ui) { internal MainInterface(PluginUi ui) {
@@ -126,18 +128,28 @@ namespace GlamourBrowser.Ui {
} }
private List<Item> GetFilteredItems() { private List<Item> GetFilteredItems() {
// Glasses are handled separately, so return empty list if on Glasses tab
if (_currentCategory == ItemCategory.Glasses) {
return new List<Item>();
}
var filter = this._itemFilter.ToLowerInvariant(); 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 items = ItemsByCategory[_currentCategory];
var filtered = string.IsNullOrEmpty(this._itemFilter) var filtered = string.IsNullOrEmpty(this._itemFilter)
? items.ToList() ? items.ToList()
: items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList(); : items.Where(item => item.Name.ExtractText().ToLowerInvariant().Contains(filter)).ToList();
FilteredItemsCache[_currentCategory] = filtered; FilteredItemsCache[_currentCategory] = filtered;
_lastFilterUsed = _itemFilter; _lastFilterUsed[_currentCategory] = _itemFilter;
} }
return FilteredItemsCache[_currentCategory]; return FilteredItemsCache[_currentCategory];
} }
@@ -296,6 +308,9 @@ namespace GlamourBrowser.Ui {
ImGui.Spacing(); ImGui.Spacing();
} }
// Draw glasses/bonus slot
DrawGlassesSlot();
ImGui.Separator(); ImGui.Separator();
ImGui.Spacing(); 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<ItemCategory, byte> stainDict, ItemCategory category, bool isPrimary) { private unsafe bool DrawDyeCombo(string id, Dictionary<ItemCategory, byte> stainDict, ItemCategory category, bool isPrimary) {
var allStains = DataCache.AllStains.Value; var allStains = DataCache.AllStains.Value;
var currentStain = stainDict[category]; 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!"); Service.ChatGui.Print("[Glamour Browser] Gear applied to character!");
} catch (Exception ex) { } catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply gear to character"); Service.Log.Error(ex, "Failed to apply gear to character");
@@ -485,6 +563,7 @@ namespace GlamourBrowser.Ui {
DrawTabButton("Gloves", ItemCategory.Gloves); DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Bottom", ItemCategory.Legs); DrawTabButton("Bottom", ItemCategory.Legs);
DrawTabButton("Shoes", ItemCategory.Feet); DrawTabButton("Shoes", ItemCategory.Feet);
DrawTabButton("Glasses", ItemCategory.Glasses);
ImGui.EndTabBar(); 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() { private void DrawItemGrid() {
// Handle glasses separately
if (_currentCategory == ItemCategory.Glasses) {
DrawGlassesGrid();
return;
}
var filteredItems = GetFilteredItems(); var filteredItems = GetFilteredItems();
if (filteredItems.Count == 0) { if (filteredItems.Count == 0) {