3 Commits

Author SHA1 Message Date
935a89847f Add dye selection functionality and related UI updates
All checks were successful
Build and Release / Build (push) Successful in 25s
2026-01-01 14:30:20 +02:00
7baa5965e4 Merge branch 'main' of ssh://gitea.lozy.pink:22222/lozy360/GlamourBrowser
All checks were successful
Build and Release / Build (push) Successful in 25s
2026-01-01 13:55:50 +02:00
667c54bc83 Reorder item categories and update related logic for gloves and body items 2026-01-01 13:55:44 +02:00
2 changed files with 206 additions and 27 deletions

View File

@@ -12,7 +12,7 @@ namespace Glamaholic {
.Where(row => row.EquipSlotCategory.RowId != 0 && .Where(row => row.EquipSlotCategory.RowId != 0 &&
row.EquipSlotCategory.Value!.SoulCrystal == 0) row.EquipSlotCategory.Value!.SoulCrystal == 0)
.ToImmutableList()); .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)!
@@ -20,7 +20,17 @@ namespace Glamaholic {
.ToImmutableDictionary(static row => .ToImmutableDictionary(static row =>
row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId)); row.Name.ExtractText().Trim().ToLower(), static row => (byte) row.RowId));
public static Lazy<ImmutableList<(byte Id, string Name, uint Color, bool Gloss)>> AllStains { get; } =
new(() => Service.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!
.Where(row => row.RowId != 0 && !row.Name.IsEmpty)
.Select(row => ((byte)row.RowId, row.Name.ExtractText(), SeColorToRgba(row.Color), row.IsMetallic))
.ToImmutableList());
// Convert SE's BGR color format to RGBA
private static uint SeColorToRgba(uint color)
=> ((color & 0xFF) << 16) | ((color >> 16) & 0xFF) | (color & 0xFF00) | 0xFF000000;
public static int GetNumStainSlots(uint itemId) => public static int GetNumStainSlots(uint itemId) =>
Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;*/ Service.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!.GetRowOrDefault(itemId)?.DyeCount ?? 0;
} }
} }

View File

@@ -18,8 +18,8 @@ namespace Glamaholic.Ui {
private enum ItemCategory { private enum ItemCategory {
Head, Head,
Gloves,
Body, Body,
Gloves,
Legs, Legs,
Feet, Feet,
} }
@@ -29,12 +29,15 @@ namespace Glamaholic.Ui {
private Dictionary<ItemCategory, Item?> SelectedItems { get; set; } = new(); private Dictionary<ItemCategory, Item?> SelectedItems { get; set; } = new();
private Dictionary<ItemCategory, List<Item>> FilteredItemsCache { get; set; } = new(); private Dictionary<ItemCategory, List<Item>> FilteredItemsCache { get; set; } = new();
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> SelectedStain2 { get; set; } = new();
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 string _lastFilterUsed = string.Empty;
private string _dyeFilter = string.Empty;
internal MainInterface(PluginUi ui) { internal MainInterface(PluginUi ui) {
this.Ui = ui; this.Ui = ui;
@@ -44,10 +47,23 @@ namespace Glamaholic.Ui {
private void InitializeSelectedItems() { private void InitializeSelectedItems() {
SelectedItems[ItemCategory.Head] = null; SelectedItems[ItemCategory.Head] = null;
SelectedItems[ItemCategory.Gloves] = null;
SelectedItems[ItemCategory.Body] = null; SelectedItems[ItemCategory.Body] = null;
SelectedItems[ItemCategory.Gloves] = null;
SelectedItems[ItemCategory.Legs] = null; SelectedItems[ItemCategory.Legs] = null;
SelectedItems[ItemCategory.Feet] = null; SelectedItems[ItemCategory.Feet] = null;
// Initialize dye selections (0 = no dye)
SelectedStain1[ItemCategory.Head] = 0;
SelectedStain1[ItemCategory.Body] = 0;
SelectedStain1[ItemCategory.Gloves] = 0;
SelectedStain1[ItemCategory.Legs] = 0;
SelectedStain1[ItemCategory.Feet] = 0;
SelectedStain2[ItemCategory.Head] = 0;
SelectedStain2[ItemCategory.Body] = 0;
SelectedStain2[ItemCategory.Gloves] = 0;
SelectedStain2[ItemCategory.Legs] = 0;
SelectedStain2[ItemCategory.Feet] = 0;
} }
private void LoadItemsByCategory() { private void LoadItemsByCategory() {
@@ -64,17 +80,6 @@ namespace Glamaholic.Ui {
.OrderBy(item => item.RowId) .OrderBy(item => item.RowId)
.ToImmutableList(); .ToImmutableList();
ItemsByCategory[ItemCategory.Gloves] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Gloves > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Body] = equippableItems ItemsByCategory[ItemCategory.Body] = equippableItems
.Where(item => { .Where(item => {
if (!item.EquipSlotCategory.IsValid) { if (!item.EquipSlotCategory.IsValid) {
@@ -86,6 +91,17 @@ namespace Glamaholic.Ui {
.OrderBy(item => item.RowId) .OrderBy(item => item.RowId)
.ToImmutableList(); .ToImmutableList();
ItemsByCategory[ItemCategory.Gloves] = equippableItems
.Where(item => {
if (!item.EquipSlotCategory.IsValid) {
return false;
}
var equipSlot = item.EquipSlotCategory.Value;
return equipSlot.Gloves > 0;
})
.OrderBy(item => item.RowId)
.ToImmutableList();
ItemsByCategory[ItemCategory.Legs] = equippableItems ItemsByCategory[ItemCategory.Legs] = equippableItems
.Where(item => { .Where(item => {
if (!item.EquipSlotCategory.IsValid) { if (!item.EquipSlotCategory.IsValid) {
@@ -204,8 +220,8 @@ namespace Glamaholic.Ui {
} }
private unsafe void DrawSelectedGearDisplay() { private unsafe void DrawSelectedGearDisplay() {
var categories = new[] { ItemCategory.Head, ItemCategory.Gloves, ItemCategory.Body, ItemCategory.Legs, ItemCategory.Feet }; var categories = new[] { ItemCategory.Head, ItemCategory.Body, ItemCategory.Gloves, ItemCategory.Legs, ItemCategory.Feet };
var categoryLabels = new[] { "Hat", "Gloves", "Top", "Bottom", "Shoes" }; var categoryLabels = new[] { "Hat", "Top", "Gloves", "Bottom", "Shoes" };
for (int i = 0; i < categories.Length; i++) { for (int i = 0; i < categories.Length; i++) {
var category = categories[i]; var category = categories[i];
@@ -213,11 +229,11 @@ namespace Glamaholic.Ui {
var selectedItem = SelectedItems[category]; var selectedItem = SelectedItems[category];
ImGui.TextUnformatted(label); ImGui.TextUnformatted(label);
var drawCursor = ImGui.GetCursorScreenPos(); var drawCursor = ImGui.GetCursorScreenPos();
var slotSize = SelectedGearIconSize + SelectedGearPaddingSize; var slotSize = SelectedGearIconSize + SelectedGearPaddingSize;
var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border); var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border);
ImGui.GetWindowDrawList().AddRect( ImGui.GetWindowDrawList().AddRect(
drawCursor, drawCursor,
drawCursor + new Vector2(slotSize), drawCursor + new Vector2(slotSize),
@@ -241,6 +257,23 @@ namespace Glamaholic.Ui {
} }
ImGui.TextUnformatted(selectedItem.Value.Name.ExtractText()); ImGui.TextUnformatted(selectedItem.Value.Name.ExtractText());
// Draw dye controls - always show both slots
ImGui.PushItemWidth(-1);
// Primary dye
if (DrawDyeCombo($"##dye1-{label}", SelectedStain1, category, true)) {
// Apply the dye change immediately
this.ApplyItemToCharacter(selectedItem.Value, category);
}
// Secondary dye
if (DrawDyeCombo($"##dye2-{label}", SelectedStain2, category, false)) {
// Apply the dye change immediately
this.ApplyItemToCharacter(selectedItem.Value, category);
}
ImGui.PopItemWidth();
} else { } else {
ImGui.SetCursorPos(cursorAfter); ImGui.SetCursorPos(cursorAfter);
ImGui.TextUnformatted("(empty)"); ImGui.TextUnformatted("(empty)");
@@ -261,6 +294,137 @@ namespace Glamaholic.Ui {
} }
} }
private unsafe bool DrawDyeCombo(string id, Dictionary<ItemCategory, byte> stainDict, ItemCategory category, bool isPrimary) {
var allStains = DataCache.AllStains.Value;
var currentStain = stainDict[category];
// Find the current stain info
string previewName = "None";
uint previewColor = 0;
bool previewGloss = false;
if (currentStain != 0) {
var stain = allStains.FirstOrDefault(s => s.Id == currentStain);
if (stain != default) {
previewName = stain.Name;
previewColor = stain.Color;
previewGloss = stain.Gloss;
}
}
// Calculate contrast color for text
var textColor = GetContrastColor(previewColor);
bool changed = false;
// Draw preview button with color
if (previewColor != 0) {
ImGui.PushStyleColor(ImGuiCol.FrameBg, previewColor);
ImGui.PushStyleColor(ImGuiCol.Text, textColor);
}
if (ImGui.BeginCombo(id, previewName, ImGuiComboFlags.HeightLarge)) {
if (previewColor != 0) {
ImGui.PopStyleColor(2);
}
// Search filter
ImGui.SetNextItemWidth(-1);
ImGui.InputTextWithHint("##dyefilter", "Search dyes...", ref _dyeFilter, 256);
ImGui.Separator();
// Filter dyes based on search
IEnumerable<(byte Id, string Name, uint Color, bool Gloss)> filteredStains = string.IsNullOrEmpty(_dyeFilter)
? allStains
: allStains.Where(s => s.Name.Contains(_dyeFilter, StringComparison.OrdinalIgnoreCase));
// None option
var noneColor = ImGui.ColorConvertFloat4ToU32(new Vector4(0.2f, 0.2f, 0.2f, 1.0f));
ImGui.PushStyleColor(ImGuiCol.Button, noneColor);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, noneColor + 0x202020);
ImGui.PushStyleColor(ImGuiCol.ButtonActive, noneColor + 0x404040);
if (ImGui.Button("None", new Vector2(-1, 0))) {
stainDict[category] = 0;
changed = true;
ImGui.CloseCurrentPopup();
}
ImGui.PopStyleColor(3);
// All filtered dyes
foreach (var stain in filteredStains) {
var stainTextColor = GetContrastColor(stain.Color);
ImGui.PushStyleColor(ImGuiCol.Button, stain.Color);
ImGui.PushStyleColor(ImGuiCol.ButtonHovered, AdjustBrightness(stain.Color, 1.1f));
ImGui.PushStyleColor(ImGuiCol.ButtonActive, AdjustBrightness(stain.Color, 0.9f));
ImGui.PushStyleColor(ImGuiCol.Text, stainTextColor);
if (ImGui.Button($"{stain.Name}##{stain.Id}", new Vector2(-1, 0))) {
stainDict[category] = stain.Id;
changed = true;
ImGui.CloseCurrentPopup();
}
// Draw gloss overlay
if (stain.Gloss) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRectFilledMultiColor(min, max,
0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000);
}
// Selection indicator
if (currentStain == stain.Id) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRect(min, max, 0xFF2020D0, 0, ImDrawFlags.None, 2.0f);
}
ImGui.PopStyleColor(4);
}
ImGui.EndCombo();
} else {
if (previewColor != 0) {
ImGui.PopStyleColor(2);
}
// Draw gloss overlay on preview
if (previewGloss) {
var drawList = ImGui.GetWindowDrawList();
var min = ImGui.GetItemRectMin();
var max = ImGui.GetItemRectMax();
drawList.AddRectFilledMultiColor(min, max,
0x50FFFFFF, 0x50000000, 0x50FFFFFF, 0x50000000);
}
}
// Right-click to clear
if (ImGui.IsItemClicked(ImGuiMouseButton.Right) && currentStain != 0) {
stainDict[category] = 0;
changed = true;
}
return changed;
}
private static uint GetContrastColor(uint rgba) {
float r = ((rgba >> 0) & 0xFF) / 255.0f;
float g = ((rgba >> 8) & 0xFF) / 255.0f;
float b = ((rgba >> 16) & 0xFF) / 255.0f;
float luminance = 0.299f * r + 0.587f * g + 0.114f * b;
return luminance > 0.5f ? 0xFF000000 : 0xFFFFFFFF;
}
private static uint AdjustBrightness(uint rgba, float factor) {
byte r = (byte)Math.Min(255, ((rgba >> 0) & 0xFF) * factor);
byte g = (byte)Math.Min(255, ((rgba >> 8) & 0xFF) * factor);
byte b = (byte)Math.Min(255, ((rgba >> 16) & 0xFF) * factor);
byte a = (byte)((rgba >> 24) & 0xFF);
return (uint)(r | (g << 8) | (b << 16) | (a << 24));
}
private void ApplySelectedGearToCharacter() { private void ApplySelectedGearToCharacter() {
if (Service.ObjectTable.LocalPlayer == null) { if (Service.ObjectTable.LocalPlayer == null) {
Service.ChatGui.PrintError("[Glamour Browser] No character found."); Service.ChatGui.PrintError("[Glamour Browser] No character found.");
@@ -278,16 +442,18 @@ namespace Glamaholic.Ui {
var slotMappings = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot> var slotMappings = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{ {
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, { ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
}; };
foreach (var (category, glamourerSlot) in slotMappings) { foreach (var (category, glamourerSlot) in slotMappings) {
if (SelectedItems[category].HasValue) { if (SelectedItems[category] is { } item) {
var item = SelectedItems[category].Value; var stain1 = SelectedStain1[category];
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); var stain2 = SelectedStain2[category];
var stains = new byte[] { stain1, stain2 };
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains);
} }
} }
@@ -299,10 +465,10 @@ namespace Glamaholic.Ui {
} }
private void DrawTabs() { private void DrawTabs() {
if (ImGui.BeginTabBar("item-category-tabs")) { if (ImGui.BeginTabBar("item-category-tabs2")) {
DrawTabButton("Hat", ItemCategory.Head); DrawTabButton("Hat", ItemCategory.Head);
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Top", ItemCategory.Body); DrawTabButton("Top", ItemCategory.Body);
DrawTabButton("Gloves", ItemCategory.Gloves);
DrawTabButton("Bottom", ItemCategory.Legs); DrawTabButton("Bottom", ItemCategory.Legs);
DrawTabButton("Shoes", ItemCategory.Feet); DrawTabButton("Shoes", ItemCategory.Feet);
@@ -434,14 +600,17 @@ namespace Glamaholic.Ui {
var slotMapping = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot> var slotMapping = new Dictionary<ItemCategory, Glamourer.Api.Enums.ApiEquipSlot>
{ {
{ ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head }, { ItemCategory.Head, Glamourer.Api.Enums.ApiEquipSlot.Head },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body }, { ItemCategory.Body, Glamourer.Api.Enums.ApiEquipSlot.Body },
{ ItemCategory.Gloves, Glamourer.Api.Enums.ApiEquipSlot.Hands },
{ ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs }, { ItemCategory.Legs, Glamourer.Api.Enums.ApiEquipSlot.Legs },
{ ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet }, { ItemCategory.Feet, Glamourer.Api.Enums.ApiEquipSlot.Feet },
}; };
if (slotMapping.TryGetValue(category, out var glamourerSlot)) { if (slotMapping.TryGetValue(category, out var glamourerSlot)) {
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, []); var stain1 = SelectedStain1[category];
var stain2 = SelectedStain2[category];
var stains = new byte[] { stain1, stain2 };
Interop.Glamourer.SetItem(playerIndex, glamourerSlot, item.RowId, stains);
} }
} catch (Exception ex) { } catch (Exception ex) {
Service.Log.Error(ex, "Failed to apply item to character"); Service.Log.Error(ex, "Failed to apply item to character");