Compare commits

...

No commits in common. "main" and "f811256f45dce306ac4acbbf0e4aa2e319aeaf2b" have entirely different histories.

19 changed files with 231 additions and 957 deletions

View File

@ -9,7 +9,7 @@ namespace Glamaholic {
this.Plugin = plugin;
this.Plugin.CommandManager.AddHandler("/glamaholic", new CommandInfo(this.OnCommand) {
HelpMessage = $"Toggle visibility of the {Plugin.Name} window",
HelpMessage = $"Toggle visibility of the {this.Plugin.Name} window",
});
}

View File

@ -13,19 +13,15 @@ namespace Glamaholic {
public bool ShowExamineMenu = true;
public bool ShowTryOnMenu = true;
public bool ShowKofiButton = true;
public bool ItemFilterShowObtainedOnly;
internal static void SanitisePlate(SavedPlate plate) {
internal void AddPlate(SavedPlate plate) {
var valid = Enum.GetValues<PlateSlot>();
foreach (var slot in plate.Items.Keys.ToArray()) {
if (!valid.Contains(slot)) {
plate.Items.Remove(slot);
}
}
}
internal void AddPlate(SavedPlate plate) {
SanitisePlate(plate);
this.Plates.Add(plate);
}
}
@ -34,7 +30,6 @@ namespace Glamaholic {
internal class SavedPlate {
public string Name { get; set; }
public Dictionary<PlateSlot, SavedGlamourItem> Items { get; init; } = new();
public List<string> Tags { get; } = new();
public SavedPlate(string name) {
this.Name = name;

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Runtime.InteropServices;
@ -8,8 +7,6 @@ using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Memory;
using Dalamud.Plugin.Services;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -19,7 +16,7 @@ using Lumina.Excel.GeneratedSheets;
namespace Glamaholic {
internal class GameFunctions : IDisposable {
private static class Signatures {
internal const string SetGlamourPlateSlot = "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B 46 10 8B 1B";
internal const string SetGlamourPlateSlot = "E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8B 46 10";
internal const string ModifyGlamourPlateSlot = "48 89 74 24 ?? 57 48 83 EC 20 80 79 30 00";
internal const string ClearGlamourPlateSlot = "80 79 30 00 4C 8B C1";
internal const string IsInArmoire = "E8 ?? ?? ?? ?? 84 C0 74 16 8B CB";
@ -28,8 +25,6 @@ namespace Glamaholic {
internal const string ExamineNamePointer = "48 8D 05 ?? ?? ?? ?? 48 89 85 ?? ?? ?? ?? 74 56 49 8B 4F";
}
#region Delegates
private delegate void SetGlamourPlateSlotDelegate(IntPtr agent, MirageSource mirageSource, int glamId, uint itemId, byte stainId);
private delegate void ModifyGlamourPlateSlotDelegate(IntPtr agent, PlateSlot slot, byte stainId, IntPtr numbers, int stainItemId);
@ -40,49 +35,33 @@ namespace Glamaholic {
private delegate byte TryOnDelegate(uint unknownCanEquip, uint itemBaseId, ulong stainColor, uint itemGlamourId, byte unknownByte);
#endregion
private Plugin Plugin { get; }
#region Functions
[Signature(Signatures.SetGlamourPlateSlot)]
private readonly SetGlamourPlateSlotDelegate _setGlamourPlateSlot = null!;
[Signature(Signatures.ModifyGlamourPlateSlot)]
private readonly ModifyGlamourPlateSlotDelegate _modifyGlamourPlateSlot = null!;
[Signature(Signatures.ClearGlamourPlateSlot)]
private readonly ClearGlamourPlateSlotDelegate _clearGlamourPlateSlot = null!;
[Signature(Signatures.IsInArmoire)]
private readonly IsInArmoireDelegate _isInArmoire = null!;
[Signature(Signatures.ArmoirePointer, ScanType = ScanType.StaticAddress)]
private readonly SetGlamourPlateSlotDelegate _setGlamourPlateSlot;
private readonly ModifyGlamourPlateSlotDelegate _modifyGlamourPlateSlot;
private readonly ClearGlamourPlateSlotDelegate _clearGlamourPlateSlot;
private readonly IsInArmoireDelegate _isInArmoire;
private readonly IntPtr _armoirePtr;
[Signature(Signatures.TryOn)]
private readonly TryOnDelegate _tryOn = null!;
[Signature(Signatures.ExamineNamePointer, ScanType = ScanType.StaticAddress)]
private readonly TryOnDelegate _tryOn;
private readonly IntPtr _examineNamePtr;
#endregion
private readonly List<uint> _filterIds = new();
internal GameFunctions(Plugin plugin) {
this.Plugin = plugin;
this.Plugin.GameInteropProvider.InitializeFromAttributes(this);
this._setGlamourPlateSlot = Marshal.GetDelegateForFunctionPointer<SetGlamourPlateSlotDelegate>(this.Plugin.SigScanner.ScanText(Signatures.SetGlamourPlateSlot));
this._modifyGlamourPlateSlot = Marshal.GetDelegateForFunctionPointer<ModifyGlamourPlateSlotDelegate>(this.Plugin.SigScanner.ScanText(Signatures.ModifyGlamourPlateSlot));
this._clearGlamourPlateSlot = Marshal.GetDelegateForFunctionPointer<ClearGlamourPlateSlotDelegate>(this.Plugin.SigScanner.ScanText(Signatures.ClearGlamourPlateSlot));
this._isInArmoire = Marshal.GetDelegateForFunctionPointer<IsInArmoireDelegate>(this.Plugin.SigScanner.ScanText(Signatures.IsInArmoire));
this._armoirePtr = this.Plugin.SigScanner.GetStaticAddressFromSig(Signatures.ArmoirePointer);
this._tryOn = Marshal.GetDelegateForFunctionPointer<TryOnDelegate>(this.Plugin.SigScanner.ScanText(Signatures.TryOn));
this._examineNamePtr = this.Plugin.SigScanner.GetStaticAddressFromSig(Signatures.ExamineNamePointer);
this.Plugin.ChatGui.ChatMessage += this.OnChat;
this.Plugin.ClientState.Login += OnLogin;
this.Plugin.Framework.Update += this.OnFrameworkUpdate;
}
public void Dispose() {
this.Plugin.Framework.Update -= this.OnFrameworkUpdate;
this.Plugin.ClientState.Login -= OnLogin;
this.Plugin.ChatGui.ChatMessage -= this.OnChat;
}
@ -96,50 +75,26 @@ namespace Glamaholic {
}
}
private static void OnLogin() {
_dresserContents = null;
}
private bool _wasEditing;
private void OnFrameworkUpdate(IFramework framework1) {
var editing = Util.IsEditingPlate(this.Plugin.GameGui);
if (!this._wasEditing && editing) {
// cache dresser
var unused = DresserContents;
}
this._wasEditing = editing;
}
internal unsafe bool ArmoireLoaded => *(byte*) this._armoirePtr > 0;
internal string? ExamineName => this._examineNamePtr == IntPtr.Zero
? null
: MemoryHelper.ReadStringNullTerminated(this._examineNamePtr);
private static readonly Stopwatch DresserTimer = new();
private static List<GlamourItem>? _dresserContents;
internal static unsafe List<GlamourItem> DresserContents {
get {
if (_dresserContents != null && DresserTimer.Elapsed < TimeSpan.FromSeconds(1)) {
return _dresserContents;
}
var list = new List<GlamourItem>();
var agents = Framework.Instance()->GetUiModule()->GetAgentModule();
var dresserAgent = agents->GetAgentByInternalId(AgentId.MiragePrismPrismBox);
// these offsets in 6.3-HF1: AD2BEB
var itemsStart = *(IntPtr*) ((IntPtr) dresserAgent + 0x28);
if (itemsStart == IntPtr.Zero) {
return _dresserContents ?? list;
return list;
}
for (var i = 0; i < 800; i++) {
var glamItem = *(GlamourItem*) (itemsStart + i * 136);
for (var i = 0; i < 400; i++) {
var glamItem = *(GlamourItem*) (itemsStart + i * 32);
if (glamItem.ItemId == 0) {
continue;
}
@ -147,9 +102,6 @@ namespace Glamaholic {
list.Add(glamItem);
}
_dresserContents = list;
DresserTimer.Restart();
return list;
}
}
@ -168,14 +120,9 @@ namespace Glamaholic {
var plate = new Dictionary<PlateSlot, SavedGlamourItem>();
foreach (var slot in (PlateSlot[]) Enum.GetValues(typeof(PlateSlot))) {
// Updated: 6.1
// from SetGlamourPlateSlot
var item = editorInfo + 44 * (int) slot + 10596;
var itemId = *(uint*) item;
var stainId = *(byte*) (item + 24);
var stainPreviewId = *(byte*) (item + 25);
var actualStainId = stainPreviewId == 0 ? stainId : stainPreviewId;
var itemId = *(uint*) (editorInfo + 44 * (int) slot + 7956);
var stainId = *(byte*) (editorInfo + 44 * (int) slot + 7980);
if (itemId == 0) {
continue;
@ -183,7 +130,7 @@ namespace Glamaholic {
plate[slot] = new SavedGlamourItem {
ItemId = itemId,
StainId = actualStainId,
StainId = stainId,
};
}
@ -191,7 +138,7 @@ namespace Glamaholic {
}
}
private static unsafe AgentInterface* EditorAgent => Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.MiragePrismMiragePlate);
private static unsafe AgentInterface* EditorAgent => Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId((AgentId) 293);
internal unsafe void SetGlamourPlateSlot(MirageSource source, int glamId, uint itemId, byte stainId) {
this._setGlamourPlateSlot((IntPtr) EditorAgent, source, glamId, itemId, stainId);
@ -228,7 +175,6 @@ namespace Glamaholic {
return;
}
// Updated: 6.11 C98BC0
var editorInfo = *(IntPtr*) ((IntPtr) agent + 0x28);
if (editorInfo == IntPtr.Zero) {
return;
@ -238,8 +184,6 @@ namespace Glamaholic {
var current = CurrentPlate;
var usedStains = new Dictionary<(uint, uint), uint>();
// Updated: 6.11 C984CF
// current plate 6.11 C9AC9F
var slotPtr = (PlateSlot*) (editorInfo + 0x18);
var initialSlot = *slotPtr;
foreach (var (slot, item) in plate.Items) {
@ -434,15 +378,15 @@ namespace Glamaholic {
}
}
[StructLayout(LayoutKind.Explicit, Size = 136)]
[StructLayout(LayoutKind.Explicit, Size = 32)]
internal readonly struct GlamourItem {
[FieldOffset(0x70)]
[FieldOffset(4)]
internal readonly uint Index;
[FieldOffset(0x74)]
[FieldOffset(8)]
internal readonly uint ItemId;
[FieldOffset(0x86)]
[FieldOffset(26)]
internal readonly byte StainId;
}

View File

@ -1,62 +1,69 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>1.9.14</Version>
<TargetFramework>net5.0-windows</TargetFramework>
<Version>1.5.0</Version>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<Nullable>enable</Nullable>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<IgnoredLints>
D0008
</IgnoredLints>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(AppData)\XIVLauncher\addon\Hooks\dev</DalamudLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)</DalamudLibPath>
<Dalamud>$(AppData)\XIVLauncher\addon\Hooks\dev</Dalamud>
</PropertyGroup>
<PropertyGroup Condition="'$(IsCI)' == 'true'">
<DalamudLibPath>$(HOME)/dalamud</DalamudLibPath>
<Dalamud>$(HOME)/dalamud</Dalamud>
</PropertyGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)\Dalamud.dll</HintPath>
<HintPath>$(Dalamud)\Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)\FFXIVClientStructs.dll</HintPath>
<HintPath>$(Dalamud)\FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)\ImGui.NET.dll</HintPath>
<HintPath>$(Dalamud)\ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGuiScene">
<HintPath>$(Dalamud)\ImGuiScene.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)\Lumina.dll</HintPath>
<HintPath>$(Dalamud)\Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)\Lumina.Excel.dll</HintPath>
<HintPath>$(Dalamud)\Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)\Newtonsoft.Json.dll</HintPath>
<HintPath>$(Dalamud)\Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12" />
<PackageReference Include="Fody" Version="6.8.0" PrivateAssets="all" />
<PackageReference Include="Resourcer.Fody" Version="1.8.1" PrivateAssets="all" />
<PackageReference Include="DalamudLinter" Version="1.0.3"/>
<PackageReference Include="DalamudPackager" Version="2.1.4"/>
<PackageReference Include="Fody" Version="6.6.0" PrivateAssets="all"/>
<PackageReference Include="Resourcer.Fody" Version="1.8.0" PrivateAssets="all"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="help.txt" />
<Content Include="..\icon.png" Link="images/icon.png" CopyToOutputDirectory="PreserveNewest" Visible="false"/>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="help.txt"/>
</ItemGroup>
</Project>

View File

@ -1,8 +1,8 @@
name: Glamaholic
author: Anna
author: ascclemens
description: |
Create and save as many glamour plates as you want. Activate up to 15 of them
at once at the Glamour Dresser. Supports exporting and importing plates for
easy sharing.
punchline: Save and swap your glamour plates.
repo_url: https://git.anna.lgbt/anna/Glamaholic
repo_url: https://git.sr.ht/~jkcclemens/Glamaholic

View File

@ -1,45 +1,33 @@
using Dalamud.Game;
using Dalamud.Data;
using Dalamud.Game;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.IoC;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
namespace Glamaholic {
// ReSharper disable once ClassNeverInstantiated.Global
public class Plugin : IDalamudPlugin {
internal static string Name => "Glamaholic";
[PluginService]
internal static IPluginLog Log { get; private set; }
internal const string PluginName = "Glamaholic";
public string Name => PluginName;
[PluginService]
internal DalamudPluginInterface Interface { get; init; }
[PluginService]
internal IChatGui ChatGui { get; init; }
internal ChatGui ChatGui { get; init; }
[PluginService]
internal IClientState ClientState { get; init; }
internal CommandManager CommandManager { get; init; }
[PluginService]
internal ICommandManager CommandManager { get; init; }
internal DataManager DataManager { get; init; }
[PluginService]
internal IDataManager DataManager { get; init; }
internal GameGui GameGui { get; init; }
[PluginService]
internal IFramework Framework { get; init; }
[PluginService]
internal IGameGui GameGui { get; init; }
[PluginService]
internal ISigScanner SigScanner { get; init; }
[PluginService]
internal ITextureProvider TextureProvider { get; init; }
[PluginService]
internal IGameInteropProvider GameInteropProvider { get; init; }
internal SigScanner SigScanner { get; init; }
internal Configuration Config { get; }
internal GameFunctions Functions { get; }

View File

@ -1,17 +1,17 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface.Internal;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using Glamaholic.Ui;
using Glamaholic.Ui.Helpers;
using ImGuiScene;
namespace Glamaholic {
internal class PluginUi : IDisposable {
internal Plugin Plugin { get; }
private Dictionary<ushort, IDalamudTextureWrap> Icons { get; } = new();
private Dictionary<ushort, TextureWrap> Icons { get; } = new();
private MainInterface MainInterface { get; }
private EditorHelper EditorHelper { get; }
@ -62,12 +62,12 @@ namespace Glamaholic {
this.MainInterface.Toggle();
}
internal IDalamudTextureWrap? GetIcon(ushort id) {
internal TextureWrap? GetIcon(ushort id) {
if (this.Icons.TryGetValue(id, out var cached)) {
return cached;
}
var icon = this.Plugin.TextureProvider.GetIcon(id);
var icon = this.Plugin.DataManager.GetImGuiTextureIcon(id);
if (icon == null) {
return null;
}
@ -96,7 +96,7 @@ namespace Glamaholic {
void SetTryOnSave(bool save) {
var tryOnAgent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Tryon);
if (tryOnAgent != IntPtr.Zero) {
*(byte*) (tryOnAgent + 0x30A) = (byte) (save ? 1 : 0);
*(byte*) (tryOnAgent + 0x2E2) = (byte) (save ? 1 : 0);
}
}

View File

@ -1,30 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
namespace Glamaholic {
[Serializable]
internal class SharedPlate {
public string Name { get; }
public Dictionary<PlateSlot, SavedGlamourItem> Items { get; }
internal SharedPlate(SavedPlate plate) {
var clone = plate.Clone();
this.Name = clone.Name;
this.Items = clone.Items;
}
[JsonConstructor]
private SharedPlate(string name, Dictionary<PlateSlot, SavedGlamourItem> items) {
this.Name = name;
this.Items = items;
}
internal SavedPlate ToPlate() {
return new SavedPlate(this.Name) {
Items = this.Items.ToDictionary(entry => entry.Key, entry => entry.Value.Clone()),
};
}
}
}

View File

@ -83,16 +83,18 @@ namespace Glamaholic.Ui {
ImGui.SameLine();
ImGui.PushFont(UiBuilder.IconFont);
if (!alt.IsDyeable) {
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
}
Util.TextIcon(FontAwesomeIcon.FillDrip);
ImGui.TextUnformatted(FontAwesomeIcon.FillDrip.ToIconString());
if (!alt.IsDyeable) {
ImGui.PopStyleColor();
}
ImGui.PopFont();
ImGui.SameLine();
ImGui.TextUnformatted(alt.Name);
}
@ -134,7 +136,7 @@ namespace Glamaholic.Ui {
var payload = new SeString(payloadList);
this.Ui.Plugin.ChatGui.Print(new XivChatEntry {
this.Ui.Plugin.ChatGui.PrintChat(new XivChatEntry {
Message = payload,
});
}

View File

@ -1,240 +0,0 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud;
using Dalamud.Plugin.Services;
using Lumina.Excel.GeneratedSheets;
namespace Glamaholic.Ui {
internal class FilterInfo {
private IDataManager Data { get; }
private uint MaxLevel { get; }
private string Query { get; }
private HashSet<ClassJob> WantedJobs { get; } = new();
private HashSet<string> Tags { get; } = new();
private HashSet<string> ExcludeTags { get; } = new();
private HashSet<uint> ItemIds { get; } = new();
private HashSet<string> ItemNames { get; } = new();
internal FilterInfo(IDataManager data, string filter) {
this.Data = data;
var queryWords = new List<string>();
var quoteType = -1;
string? quoted = null;
foreach (var immutableWord in filter.Split(' ')) {
var word = immutableWord;
if (quoted != null) {
quoted += " ";
var quoteIndex = word.IndexOf('"');
if (quoteIndex > -1) {
quoted += word[..quoteIndex];
switch (quoteType) {
case 1:
this.Tags.Add(quoted);
break;
case 2:
this.ItemNames.Add(quoted);
break;
case 3:
this.ExcludeTags.Add(quoted);
break;
}
quoted = null;
quoteType = -1;
var rest = word[(quoteIndex + 1)..];
if (rest.Length > 0) {
word = rest;
} else {
continue;
}
} else {
quoted += word;
continue;
}
}
if (word.StartsWith("j:")) {
var abbr = word[2..].ToLowerInvariant();
var job = this.Data.GetExcelSheet<ClassJob>()!.FirstOrDefault(row => row.Abbreviation.RawString.ToLowerInvariant() == abbr);
if (job != null) {
this.WantedJobs.Add(job);
}
continue;
}
if (word.StartsWith("lvl:")) {
if (uint.TryParse(word[4..], out var level)) {
this.MaxLevel = level;
}
continue;
}
if (word.StartsWith("t:")) {
if (word.StartsWith("t:\"")) {
if (word.EndsWith('"') && word.Length >= 5) {
this.Tags.Add(word[3..^1]);
} else {
quoteType = 1;
quoted = word[3..];
}
} else {
this.Tags.Add(word[2..]);
}
continue;
}
if (word.StartsWith("!t:")) {
if (word.StartsWith("!t:\"")) {
if (word.EndsWith('"') && word.Length >= 6) {
this.ExcludeTags.Add(word[4..^1]);
} else {
quoteType = 3;
quoted = word[4..];
}
} else {
this.ExcludeTags.Add(word[3..]);
}
continue;
}
if (word.StartsWith("id:")) {
if (uint.TryParse(word[3..], out var id)) {
this.ItemIds.Add(id);
}
continue;
}
if (word.StartsWith("i:")) {
if (word.StartsWith("i:\"")) {
if (word.EndsWith('"') && word.Length >= 5) {
this.ItemNames.Add(word[3..^1]);
} else {
quoteType = 2;
quoted = word[3..];
}
} else {
this.ItemNames.Add(word[2..]);
}
continue;
}
queryWords.Add(word);
}
this.Query = string.Join(' ', queryWords).ToLowerInvariant();
}
internal bool Matches(SavedPlate plate) {
// if the name doesn't match the query, it's not a match, obviously
if (this.Query.Length != 0 && !plate.Name.ToLowerInvariant().Contains(this.Query)) {
return false;
}
// if there's nothing custom about this filter, this is a match
var notCustom = this.MaxLevel == 0
&& this.WantedJobs.Count == 0
&& this.Tags.Count == 0
&& this.ExcludeTags.Count == 0
&& this.ItemIds.Count == 0
&& this.ItemNames.Count == 0;
if (notCustom) {
return true;
}
foreach (var tag in this.Tags) {
if (!plate.Tags.Contains(tag)) {
return false;
}
}
foreach (var tag in this.ExcludeTags) {
if (plate.Tags.Contains(tag)) {
return false;
}
}
if (this.ItemIds.Count > 0) {
var matching = plate.Items.Values
.Select(mirage => mirage.ItemId)
.Intersect(this.ItemIds)
.Count();
if (matching != this.ItemIds.Count) {
return false;
}
}
if (this.ItemNames.Count > 0) {
var sheet = this.Data.GetExcelSheet<Item>()!;
var names = plate.Items.Values
.Select(mirage => sheet.GetRow(mirage.ItemId % Util.HqItemOffset))
.Where(item => item != null)
.Cast<Item>()
.Select(item => item.Name.RawString.ToLowerInvariant())
.ToArray();
foreach (var needle in this.ItemNames) {
var lower = needle.ToLowerInvariant();
if (!names.Any(name => name.Contains(lower))) {
return false;
}
}
}
foreach (var mirage in plate.Items.Values) {
var item = this.Data.GetExcelSheet<Item>()!.GetRow(mirage.ItemId % Util.HqItemOffset);
if (item == null) {
continue;
}
if (this.MaxLevel != 0 && item.LevelEquip > this.MaxLevel) {
return false;
}
foreach (var job in this.WantedJobs) {
var category = item.ClassJobCategory.Value;
if (category == null) {
continue;
}
if (!this.CanWear(category, job)) {
return false;
}
}
}
return true;
}
private bool CanWear(ClassJobCategory category, ClassJob classJob) {
// get english version
var job = this.Data.GetExcelSheet<ClassJob>(ClientLanguage.English)!.GetRow(classJob.RowId)!;
var getter = category.GetType().GetProperty(job.Abbreviation.RawString, BindingFlags.Public | BindingFlags.Instance);
if (getter == null) {
return false;
}
var value = getter.GetValue(category);
if (value is bool res) {
return res;
}
return false;
}
}
}

View File

@ -4,7 +4,6 @@ using ImGuiNET;
namespace Glamaholic.Ui.Helpers {
internal class EditorHelper {
private PluginUi Ui { get; }
private string _plateName = string.Empty;
internal EditorHelper(PluginUi ui) {
this.Ui = ui;
@ -25,13 +24,9 @@ namespace Glamaholic.Ui.Helpers {
}
private void DrawDropdown() {
if (ImGui.Selectable($"Open {Plugin.Name}")) {
if (ImGui.Selectable($"Open {this.Ui.Plugin.Name}")) {
this.Ui.OpenMainInterface();
}
if (HelperUtil.DrawCreatePlateMenu(this.Ui, () => GameFunctions.CurrentPlate, ref this._plateName)) {
this._plateName = string.Empty;
}
}
}
}

View File

@ -6,7 +6,6 @@ using ImGuiNET;
namespace Glamaholic.Ui.Helpers {
internal class ExamineHelper {
private PluginUi Ui { get; }
private string _nameInput = string.Empty;
internal ExamineHelper(PluginUi ui) {
this.Ui = ui;
@ -27,16 +26,10 @@ namespace Glamaholic.Ui.Helpers {
}
private void DrawDropdown() {
if (ImGui.Selectable($"Open {Plugin.Name}")) {
this.Ui.OpenMainInterface();
if (ImGui.Selectable("Create glamour plate")) {
this.CopyToGlamourPlate();
}
if (ImGui.IsWindowAppearing()) {
this._nameInput = this.Ui.Plugin.Functions.ExamineName ?? "Copied glamour";
}
HelperUtil.DrawCreatePlateMenu(this.Ui, GetItems, ref this._nameInput);
if (ImGui.Selectable("Try on")) {
var items = GetItems();
if (items != null) {
@ -65,7 +58,7 @@ namespace Glamaholic.Ui.Helpers {
var stainId = item.Stain;
// for some reason, this still accounts for belts in EW
// TODO: remove this logic in endwalker
var slot = i > 5 ? i - 1 : i;
items[(PlateSlot) slot] = new SavedGlamourItem {
ItemId = itemId,
@ -75,5 +68,31 @@ namespace Glamaholic.Ui.Helpers {
return items;
}
private unsafe void CopyToGlamourPlate() {
var inventory = InventoryManager.Instance()->GetInventoryContainer(InventoryType.Examine);
if (inventory == null) {
return;
}
var name = this.Ui.Plugin.Functions.ExamineName;
if (string.IsNullOrEmpty(name)) {
name = "Copied glamour";
}
var items = GetItems();
if (items == null) {
return;
}
var plate = new SavedPlate(name) {
Items = items,
};
this.Ui.Plugin.Config.AddPlate(plate);
this.Ui.Plugin.SaveConfig();
this.Ui.OpenMainInterface();
this.Ui.SwitchPlate(this.Ui.Plugin.Config.Plates.Count - 1, true);
}
}
}

View File

@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.Numerics;
using Dalamud.Interface.Utility;
using Dalamud.Interface;
using Dalamud.Logging;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
@ -44,7 +44,7 @@ namespace Glamaholic.Ui.Helpers {
internal static float DropdownWidth() {
// arrow size is GetFrameHeight
return (ImGui.CalcTextSize(Plugin.Name).X + ImGui.GetStyle().ItemInnerSpacing.X * 2 + ImGui.GetFrameHeight()) * ImGuiHelpers.GlobalScale;
return (ImGui.CalcTextSize(Plugin.PluginName).X + ImGui.GetStyle().ItemInnerSpacing.X * 2 + ImGui.GetFrameHeight()) * ImGuiHelpers.GlobalScale;
}
internal class HelperStyles : IDisposable {
@ -75,11 +75,11 @@ namespace Glamaholic.Ui.Helpers {
}
ImGui.SetNextItemWidth(DropdownWidth());
if (ImGui.BeginCombo($"##{id}-combo", Plugin.Name)) {
if (ImGui.BeginCombo($"##{id}-combo", Plugin.PluginName)) {
try {
dropdown();
} catch (Exception ex) {
Plugin.Log.Error(ex, "Error drawing helper combo");
PluginLog.LogError(ex, "Error drawing helper combo");
}
ImGui.EndCombo();
@ -89,75 +89,5 @@ namespace Glamaholic.Ui.Helpers {
ImGui.End();
}
internal static bool DrawCreatePlateMenu(PluginUi ui, Func<Dictionary<PlateSlot, SavedGlamourItem>?> getter, ref string nameInput) {
var ret = false;
if (!ImGui.BeginMenu("Create glamour plate")) {
return ret;
}
const string msg = "Enter a name and press Enter to create a new plate, or choose a plate below to overwrite.";
ImGui.PushTextWrapPos(250);
if (Util.DrawTextInput("current-name", ref nameInput, message: msg, flags: ImGuiInputTextFlags.AutoSelectAll)) {
var items = getter();
if (items != null) {
CopyToGlamourPlate(ui, nameInput, items, -1);
ret = true;
}
}
ImGui.PopTextWrapPos();
if (ImGui.IsWindowAppearing()) {
ImGui.SetKeyboardFocusHere(-1);
}
ImGui.Separator();
if (ImGui.BeginChild("helper-overwrite", new Vector2(250, 350))) {
for (var i = 0; i < ui.Plugin.Config.Plates.Count; i++) {
var plate = ui.Plugin.Config.Plates[i];
var ctrl = ImGui.GetIO().KeyCtrl;
if (ImGui.Selectable($"{plate.Name}##{i}") && ctrl) {
var items = getter();
if (items != null) {
CopyToGlamourPlate(ui, plate.Name, items, i);
ret = true;
}
}
if (!ctrl && ImGui.IsItemHovered()) {
ImGui.BeginTooltip();
ImGui.TextUnformatted("Hold Control and click to overwrite.");
ImGui.EndTooltip();
}
}
ImGui.EndChild();
}
ImGui.EndMenu();
return ret;
}
private static void CopyToGlamourPlate(PluginUi ui, string name, Dictionary<PlateSlot, SavedGlamourItem> items, int idx) {
var plate = new SavedPlate(name) {
Items = items,
};
Configuration.SanitisePlate(plate);
if (idx == -1) {
ui.Plugin.Config.AddPlate(plate);
} else {
ui.Plugin.Config.Plates[idx] = plate;
}
ui.Plugin.SaveConfig();
ui.OpenMainInterface();
ui.SwitchPlate(idx == -1 ? ui.Plugin.Config.Plates.Count - 1 : idx, true);
}
}
}

View File

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using FFXIVClientStructs.FFXIV.Client.System.Framework;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
@ -9,10 +8,7 @@ using ImGuiNET;
namespace Glamaholic.Ui.Helpers {
internal class TryOnHelper {
private const string PlateName = "Fitting Room";
private PluginUi Ui { get; }
private string _nameInput = PlateName;
internal TryOnHelper(PluginUi ui) {
this.Ui = ui;
@ -29,32 +25,30 @@ namespace Glamaholic.Ui.Helpers {
return;
}
var right = this.Ui.Plugin.Interface.InstalledPlugins.Any(state => state.InternalName == "ItemSearchPlugin");
var right = this.Ui.Plugin.Interface.PluginInternalNames.Contains("ItemSearchPlugin");
HelperUtil.DrawHelper(tryOnAddon, "glamaholic-helper-try-on", right, this.DrawDropdown);
}
private void DrawDropdown() {
if (ImGui.Selectable($"Open {Plugin.Name}")) {
if (ImGui.Selectable("Create glamour plate")) {
this.Ui.Plugin.Config.AddPlate(new SavedPlate("Fitting Room") {
Items = GetTryOnItems(),
});
this.Ui.Plugin.SaveConfig();
this.Ui.OpenMainInterface();
}
if (ImGui.IsWindowAppearing()) {
this._nameInput = PlateName;
}
if (HelperUtil.DrawCreatePlateMenu(this.Ui, GetTryOnItems, ref this._nameInput)) {
this._nameInput = PlateName;
this.Ui.SwitchPlate(this.Ui.Plugin.Config.Plates.Count - 1, true);
}
}
private static unsafe Dictionary<PlateSlot, SavedGlamourItem> GetTryOnItems() {
var agent = (IntPtr) Framework.Instance()->GetUiModule()->GetAgentModule()->GetAgentByInternalId(AgentId.Tryon);
var firstItem = agent + 0x314;
var firstItem = agent + 0x2E8;
var items = new Dictionary<PlateSlot, SavedGlamourItem>();
for (var i = 0; i < 12; i++) {
var item = (TryOnItem*) (firstItem + i * 28);
var item = (TryOnItem*) (firstItem + i * 24);
if (item->Slot == 14 || item->ItemId == 0) {
continue;
}
@ -63,23 +57,19 @@ namespace Glamaholic.Ui.Helpers {
if (item->GlamourId != 0) {
itemId = item->GlamourId;
}
var stainId = item->StainPreviewId == 0
? item->StainId
: item->StainPreviewId;
// for some reason, this still accounts for belts in EW
// TODO: remove this logic in endwalker
var slot = item->Slot > 5 ? item->Slot - 1 : item->Slot;
items[(PlateSlot) slot] = new SavedGlamourItem {
items[(PlateSlot) slot] =new SavedGlamourItem {
ItemId = itemId % Util.HqItemOffset,
StainId = stainId,
StainId = item->StainId,
};
}
return items;
}
[StructLayout(LayoutKind.Explicit, Size = 28)]
[StructLayout(LayoutKind.Explicit, Size = 24)]
private readonly struct TryOnItem {
[FieldOffset(0)]
internal readonly byte Slot;
@ -87,16 +77,13 @@ namespace Glamaholic.Ui.Helpers {
[FieldOffset(2)]
internal readonly byte StainId;
[FieldOffset(3)]
internal readonly byte StainPreviewId;
[FieldOffset(5)]
internal readonly byte UnknownByte;
[FieldOffset(12)]
[FieldOffset(8)]
internal readonly uint ItemId;
[FieldOffset(16)]
[FieldOffset(12)]
internal readonly uint GlamourId;
}
}

View File

@ -2,14 +2,9 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Numerics;
using System.Threading.Tasks;
using Dalamud;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
using Newtonsoft.Json;
@ -39,39 +34,32 @@ namespace Glamaholic.Ui {
private PluginUi Ui { get; }
private List<Item> Items { get; }
private List<Item> FilteredItems { get; set; }
private Dictionary<string, byte> Stains { get; }
private FilterInfo? PlateFilter { get; set; }
private bool _visible;
private string _plateName = string.Empty;
private int _dragging = -1;
private int _selectedPlate = -1;
private bool _scrollToSelected;
private string _plateFilter = string.Empty;
private bool _showRename;
private string _renameInput = string.Empty;
private string _importInput = string.Empty;
private Exception? _importError;
private bool _deleteConfirm;
private readonly Stopwatch _shareTimer = new();
private bool _editing;
private SavedPlate? _editingPlate;
private string _itemFilter = string.Empty;
private string _dyeFilter = string.Empty;
private volatile bool _ecImporting;
private readonly Dictionary<string, Stopwatch> _timedMessages = new();
private string _tagInput = string.Empty;
internal MainInterface(PluginUi ui) {
this.Ui = ui;
// get all equippable items that aren't soul crystals
this.Items = this.Ui.Plugin.DataManager.GetExcelSheet<Item>(ClientLanguage.English)!
this.Items = this.Ui.Plugin.DataManager.GetExcelSheet<Item>()!
.Where(row => row.EquipSlotCategory.Row is not 0 && row.EquipSlotCategory.Value!.SoulCrystal == 0)
.ToList();
this.FilteredItems = this.Items;
this.Stains = this.Ui.Plugin.DataManager.GetExcelSheet<Stain>(ClientLanguage.English)!
.Where(row => row.RowId != 0)
.Where(row => !string.IsNullOrWhiteSpace(row.Name.RawString))
.ToDictionary(row => row.Name.RawString, row => (byte) row.RowId);
}
internal void Open() {
@ -91,7 +79,7 @@ namespace Glamaholic.Ui {
ImGui.SetNextWindowSize(new Vector2(415, 650), ImGuiCond.FirstUseEver);
if (!ImGui.Begin(Plugin.Name, ref this._visible, ImGuiWindowFlags.MenuBar)) {
if (!ImGui.Begin(this.Ui.Plugin.Name, ref this._visible, ImGuiWindowFlags.MenuBar)) {
ImGui.End();
return;
}
@ -101,14 +89,6 @@ namespace Glamaholic.Ui {
ImGui.End();
}
private static bool IsValidEorzeaCollectionUrl(string urlString) {
if (!Uri.TryCreate(urlString, UriKind.Absolute, out var url)) {
return false;
}
return url.Host == "ffxiv.eorzeacollection.com" && url.AbsolutePath.StartsWith("/glamour/");
}
private void DrawMenuBar() {
if (!ImGui.BeginMenuBar()) {
return;
@ -121,24 +101,53 @@ namespace Glamaholic.Ui {
this.SwitchPlate(this.Ui.Plugin.Config.Plates.Count - 1, true);
}
if (ImGui.BeginMenu("Import")) {
if (ImGui.MenuItem("Clipboard")) {
var json = Util.GetClipboardText();
try {
var plate = JsonConvert.DeserializeObject<SharedPlate>(json);
if (plate != null) {
this.Ui.Plugin.Config.AddPlate(plate.ToPlate());
this.Ui.Plugin.SaveConfig();
this.Ui.SwitchPlate(this.Ui.Plugin.Config.Plates.Count - 1);
}
} catch (Exception ex) {
Plugin.Log.Warning(ex, "Failed to import glamour plate");
if (ImGui.BeginMenu("Add from current plate")) {
if (Util.DrawTextInput("current-name", ref this._plateName, message: "Input name and press Enter to save.")) {
var current = GameFunctions.CurrentPlate;
if (current != null) {
var plate = new SavedPlate(this._plateName) {
Items = current,
};
this.Ui.Plugin.Config.AddPlate(plate);
this._plateName = string.Empty;
this.Ui.Plugin.SaveConfig();
}
}
var validUrl = IsValidEorzeaCollectionUrl(Util.GetClipboardText());
if (ImGui.MenuItem("Copied Eorzea Collection URL", validUrl) && !this._ecImporting) {
this.ImportEorzeaCollection(Util.GetClipboardText());
if (ImGui.IsWindowAppearing()) {
ImGui.SetKeyboardFocusHere();
}
ImGui.EndMenu();
}
if (ImGui.BeginMenu("Import")) {
if (Util.DrawTextInput("import-input", ref this._importInput, 2048, "Press Enter to import.")) {
try {
var plate = JsonConvert.DeserializeObject<SavedPlate>(this._importInput);
this._importError = null;
if (plate != null) {
this.Ui.Plugin.Config.AddPlate(plate);
this.Ui.Plugin.SaveConfig();
}
this._importInput = string.Empty;
} catch (Exception ex) {
this._importError = ex;
}
}
if (ImGui.IsWindowAppearing()) {
this._importError = null;
ImGui.SetKeyboardFocusHere();
}
if (this._importError != null) {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudRed);
Util.TextUnformattedWrapped(this._importError.Message);
ImGui.PopStyleColor();
}
ImGui.EndMenu();
@ -185,7 +194,7 @@ namespace Glamaholic.Ui {
ImGui.PushStyleColor(ImGuiCol.Text, 0xFFFFFFFF);
ImGui.PushStyleColor(ImGuiCol.HeaderHovered, 0x00000000);
if (ImGui.MenuItem(kofiText)) {
Process.Start(new ProcessStartInfo("https://ko-fi.com/lojewalo") {
Process.Start(new ProcessStartInfo("https://ko-fi.com/ascclemens") {
UseShellExecute = true,
});
}
@ -200,105 +209,21 @@ namespace Glamaholic.Ui {
ImGui.EndMenuBar();
}
private void ImportEorzeaCollection(string url) {
if (!IsValidEorzeaCollectionUrl(url)) {
return;
}
this._ecImporting = true;
Task.Run(async () => {
var items = new Dictionary<PlateSlot, SavedGlamourItem>();
var client = new HttpClient();
var resp = await client.GetAsync(url);
var html = await resp.Content.ReadAsStringAsync();
var titleParts = html.Split("<title>");
var glamName = titleParts.Length > 1
? WebUtility.HtmlDecode(titleParts[1].Split('<')[0].Split('|')[0].Trim())
: "Eorzea Collection plate";
var parts = html.Split("c-gear-slot-item-name");
foreach (var part in parts) {
var nameParts = part.Split('>');
if (nameParts.Length < 2) {
continue;
}
var rawName = nameParts[1].Split('<')[0].Trim();
var name = WebUtility.HtmlDecode(rawName);
if (string.IsNullOrWhiteSpace(name)) {
continue;
}
var item = this.Items.Find(item => item.Name == name);
if (item == null) {
continue;
}
var slot = Util.GetSlot(item);
if (slot is PlateSlot.RightRing && items.ContainsKey(PlateSlot.RightRing)) {
slot = PlateSlot.LeftRing;
}
if (slot == null) {
continue;
}
var stainId = item.IsDyeable ? this.GetStainIdFromPart(part) : (byte) 0;
items[slot.Value] = new SavedGlamourItem {
ItemId = item.RowId,
StainId = stainId,
};
}
this._ecImporting = false;
var plate = new SavedPlate(glamName) {
Items = items,
};
this.Ui.Plugin.Config.AddPlate(plate);
this.Ui.Plugin.SaveConfig();
this.SwitchPlate(this.Ui.Plugin.Config.Plates.Count - 1, true);
});
}
private byte GetStainIdFromPart(string part) {
var stainParts = part.Split('⬤');
if (stainParts.Length <= 1) {
return 0;
}
var stainSubParts = stainParts[1].Split('>');
if (stainSubParts.Length <= 1) {
return 0;
}
var rawStainName = stainSubParts[1].Split('<')[0].Trim();
var stainName = WebUtility.HtmlDecode(rawStainName);
this.Stains.TryGetValue(stainName, out var stainId);
return stainId;
}
private void DrawPlateList() {
if (!ImGui.BeginChild("plate list", new Vector2(205 * ImGuiHelpers.GlobalScale, 0), true)) {
return;
}
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint("##plate-filter", "Search...", ref this._plateFilter, 512, ImGuiInputTextFlags.AutoSelectAll)) {
this.PlateFilter = this._plateFilter.Length == 0
? null
: new FilterInfo(this.Ui.Plugin.DataManager, this._plateFilter);
}
ImGui.InputText("##plate-filter", ref this._plateFilter, 512, ImGuiInputTextFlags.AutoSelectAll);
var filter = this._plateFilter.ToLowerInvariant();
(int src, int dst)? drag = null;
if (ImGui.BeginChild("plate list actual", Vector2.Zero, false, ImGuiWindowFlags.HorizontalScrollbar)) {
for (var i = 0; i < this.Ui.Plugin.Config.Plates.Count; i++) {
var plate = this.Ui.Plugin.Config.Plates[i];
if (this.PlateFilter != null && !this.PlateFilter.Matches(plate)) {
if (filter.Length != 0 && !plate.Name.ToLowerInvariant().Contains(filter)) {
continue;
}
@ -410,7 +335,7 @@ namespace Glamaholic.Ui {
ImGui.InputText("##dye-filter", ref this._dyeFilter, 512);
if (ImGui.IsWindowAppearing()) {
ImGui.SetKeyboardFocusHere(-1);
ImGui.SetKeyboardFocusHere();
}
if (ImGui.BeginChild("dye picker", new Vector2(250, 350), false, ImGuiWindowFlags.HorizontalScrollbar)) {
@ -448,21 +373,12 @@ namespace Glamaholic.Ui {
}
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint("##item-filter", "Search...", ref this._itemFilter, 512, ImGuiInputTextFlags.AutoSelectAll)) {
if (ImGui.InputText("##item-filter", ref this._itemFilter, 512, ImGuiInputTextFlags.AutoSelectAll)) {
this.FilterItems(slot);
}
if (ImGui.IsWindowAppearing()) {
ImGui.SetKeyboardFocusHere(-1);
}
if (GameFunctions.DresserContents.Count > 0) {
if (ImGui.Checkbox("Only show items in the armoire/dresser", ref this.Ui.Plugin.Config.ItemFilterShowObtainedOnly)) {
this.Ui.Plugin.SaveConfig();
this.FilterItems(slot);
}
ImGui.Separator();
ImGui.SetKeyboardFocusHere();
}
if (ImGui.BeginChild("item search", new Vector2(250, 450), false, ImGuiWindowFlags.HorizontalScrollbar)) {
@ -473,26 +389,16 @@ namespace Glamaholic.Ui {
id = null;
}
if (ImGui.Selectable("##none-keep", id == null)) {
if (ImGui.Selectable("None (keep existing)", id == null)) {
plate.Items.Remove(slot);
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
Util.TextIcon(FontAwesomeIcon.Box);
ImGui.SameLine();
ImGui.TextUnformatted("None (keep existing)");
if (ImGui.Selectable("##none-remove)", id == 0)) {
if (ImGui.Selectable("None (remove existing)", id == 0)) {
plate.Items[slot] = new SavedGlamourItem();
ImGui.CloseCurrentPopup();
}
ImGui.SameLine();
Util.TextIcon(FontAwesomeIcon.Box);
ImGui.SameLine();
ImGui.TextUnformatted("None (remove existing)");
var clipper = new ImGuiListClipperPtr(ImGuiNative.ImGuiListClipper_ImGuiListClipper());
clipper.Begin(this.FilteredItems.Count);
@ -500,7 +406,7 @@ namespace Glamaholic.Ui {
for (var i = clipper.DisplayStart; i < clipper.DisplayEnd; i++) {
var item = this.FilteredItems[i];
if (ImGui.Selectable($"##{item.RowId}", item.RowId == id)) {
if (ImGui.Selectable($"{item.Name}##{item.RowId}", item.RowId == id)) {
if (!plate.Items.ContainsKey(slot)) {
plate.Items[slot] = new SavedGlamourItem();
}
@ -516,24 +422,6 @@ namespace Glamaholic.Ui {
if (Util.IsItemMiddleOrCtrlClicked()) {
this.Ui.AlternativeFinders.Add(new AlternativeFinder(this.Ui, item));
}
ImGui.SameLine();
var has = GameFunctions.DresserContents.Any(saved => saved.ItemId % Util.HqItemOffset == item.RowId) || this.Ui.Plugin.Functions.IsInArmoire(item.RowId);
if (!has) {
ImGui.PushStyleColor(ImGuiCol.Text, ImGui.GetStyle().Colors[(int) ImGuiCol.TextDisabled]);
}
Util.TextIcon(FontAwesomeIcon.Box);
if (!has) {
ImGui.PopStyleColor();
}
ImGui.SameLine();
ImGui.TextUnformatted($"{item.Name}");
}
}
@ -543,7 +431,7 @@ namespace Glamaholic.Ui {
ImGui.EndPopup();
}
private unsafe void DrawIcon(PlateSlot slot, SavedPlate plate, int iconSize, int paddingSize) {
private unsafe void DrawIcon(PlateSlot slot, SavedPlate plate, bool editingPlate, int iconSize, int paddingSize) {
var drawCursor = ImGui.GetCursorScreenPos();
var tooltip = slot.Name();
ImGui.BeginGroup();
@ -553,7 +441,7 @@ namespace Glamaholic.Ui {
var borderColour = *ImGui.GetStyleColorVec4(ImGuiCol.Border);
// check for item
if (mirage != null && mirage.ItemId != 0 && GameFunctions.DresserContents.Count > 0) {
if (mirage != null && mirage.ItemId != 0 && editingPlate) {
var has = GameFunctions.DresserContents.Any(saved => saved.ItemId % Util.HqItemOffset == mirage.ItemId) || this.Ui.Plugin.Functions.IsInArmoire(mirage.ItemId);
if (!has) {
borderColour = ImGuiColors.DalamudYellow;
@ -650,7 +538,7 @@ namespace Glamaholic.Ui {
}
}
private void DrawPlatePreview(SavedPlate plate) {
private void DrawPlatePreview(bool editingPlate, SavedPlate plate) {
const int paddingSize = 12;
if (!ImGui.BeginTable("plate item preview", 2, ImGuiTableFlags.SizingFixedFit)) {
@ -659,9 +547,9 @@ namespace Glamaholic.Ui {
foreach (var (left, right) in LeftSide.Zip(RightSide)) {
ImGui.TableNextColumn();
this.DrawIcon(left, plate, IconSize, paddingSize);
this.DrawIcon(left, plate, editingPlate, IconSize, paddingSize);
ImGui.TableNextColumn();
this.DrawIcon(right, plate, IconSize, paddingSize);
this.DrawIcon(right, plate, editingPlate, IconSize, paddingSize);
}
ImGui.EndTable();
@ -674,11 +562,7 @@ namespace Glamaholic.Ui {
ImGui.TableNextColumn();
if (Util.IconButton(FontAwesomeIcon.Check, tooltip: "Apply")) {
if (!Util.IsEditingPlate(this.Ui.Plugin.GameGui)) {
this.AddTimedMessage("The in-game plate editor must be open.");
} else {
this.Ui.Plugin.Functions.LoadPlate(plate);
}
this.Ui.Plugin.Functions.LoadPlate(plate);
}
ImGui.TableNextColumn();
@ -700,60 +584,14 @@ namespace Glamaholic.Ui {
ImGui.TableNextColumn();
if (Util.IconButton(FontAwesomeIcon.ShareAltSquare, tooltip: "Share")) {
ImGui.SetClipboardText(JsonConvert.SerializeObject(new SharedPlate(plate)));
this.AddTimedMessage("Copied to clipboard.");
ImGui.SetClipboardText(JsonConvert.SerializeObject(plate));
this._shareTimer.Start();
}
ImGui.EndTable();
}
private void DrawPlateTags(SavedPlate plate) {
if (this._editing) {
return;
}
if (!ImGui.CollapsingHeader($"Tags ({plate.Tags.Count})###plate-tags")) {
return;
}
ImGui.SetNextItemWidth(-1);
if (ImGui.InputTextWithHint("##tag-input", "Input a tag and press Enter", ref this._tagInput, 128, ImGuiInputTextFlags.EnterReturnsTrue)) {
if (!string.IsNullOrWhiteSpace(this._tagInput)) {
var tag = this._tagInput.Trim();
if (!plate.Tags.Contains(tag)) {
plate.Tags.Add(tag);
plate.Tags.Sort();
this.Ui.Plugin.SaveConfig();
}
}
this._tagInput = string.Empty;
}
if (ImGui.BeginChild("tag-list")) {
var toRemove = -1;
for (var i = 0; i < plate.Tags.Count; i++) {
var tag = plate.Tags[i];
if (Util.IconButton(FontAwesomeIcon.Times, $"remove-tag-{i}")) {
toRemove = i;
}
ImGui.SameLine();
ImGui.TextUnformatted(tag);
}
if (toRemove > -1) {
plate.Tags.RemoveAt(toRemove);
this.Ui.Plugin.SaveConfig();
}
ImGui.EndChild();
}
}
private void DrawPlateDetail() {
private void DrawPlateDetail(bool editingPlate) {
if (!ImGui.BeginChild("plate detail")) {
return;
}
@ -761,14 +599,14 @@ namespace Glamaholic.Ui {
if (this._selectedPlate > -1 && this._selectedPlate < this.Ui.Plugin.Config.Plates.Count) {
var plate = this._editingPlate ?? this.Ui.Plugin.Config.Plates[this._selectedPlate];
this.DrawPlatePreview(plate);
this.DrawPlatePreview(editingPlate, plate);
var renameWasVisible = this._showRename;
this.DrawPlateButtons(plate);
foreach (var (msg, _) in this._timedMessages) {
Util.TextUnformattedWrapped(msg);
if (this._shareTimer.IsRunning) {
Util.TextUnformattedWrapped("Copied to clipboard.");
}
if (this._showRename && Util.DrawTextInput("plate-rename", ref this._renameInput, flags: ImGuiInputTextFlags.AutoSelectAll)) {
@ -778,7 +616,13 @@ namespace Glamaholic.Ui {
}
if (this._showRename && !renameWasVisible) {
ImGui.SetKeyboardFocusHere(-1);
ImGui.SetKeyboardFocusHere();
}
if (!this.Ui.Plugin.Functions.ArmoireLoaded) {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
Util.TextUnformattedWrapped("The Armoire is not loaded. Open it once to enable glamours from the Armoire.");
ImGui.PopStyleColor();
}
if (this._editing) {
@ -796,81 +640,44 @@ namespace Glamaholic.Ui {
this.ResetEditing();
}
}
this.DrawPlateTags(plate);
}
ImGui.EndChild();
}
private void DrawWarnings() {
var warnings = new List<string>();
if (!this.Ui.Plugin.Functions.ArmoireLoaded) {
warnings.Add("The Armoire is not loaded. Open it once to enable glamours from the Armoire.");
}
if (GameFunctions.DresserContents.Count == 0) {
warnings.Add("Glamour Dresser is empty or has not been opened. Glamaholic will not know which items you have.");
}
if (warnings.Count == 0) {
return;
}
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
var header = ImGui.CollapsingHeader($"Warnings ({warnings.Count})###warnings");
ImGui.PopStyleColor();
if (!header) {
return;
}
for (var i = 0; i < warnings.Count; i++) {
if (i != 0) {
ImGui.Separator();
}
Util.TextUnformattedWrapped(warnings[i]);
}
}
private void DrawInner() {
var editingPlate = Util.IsEditingPlate(this.Ui.Plugin.GameGui);
this.DrawMenuBar();
this.DrawWarnings();
if (!editingPlate) {
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.DalamudYellow);
Util.TextUnformattedWrapped("Glamour Plate editor is not open. Certain functions will not work.");
ImGui.PopStyleColor();
}
this.DrawPlateList();
ImGui.SameLine();
this.DrawPlateDetail();
this.DrawPlateDetail(editingPlate);
ImGui.End();
}
private void HandleTimers() {
var keys = this._timedMessages.Keys.ToArray();
foreach (var key in keys) {
if (this._timedMessages[key].Elapsed > TimeSpan.FromSeconds(5)) {
this._timedMessages.Remove(key);
}
if (this._shareTimer.Elapsed > TimeSpan.FromSeconds(5)) {
this._shareTimer.Reset();
}
}
private void AddTimedMessage(string message) {
var timer = new Stopwatch();
timer.Start();
this._timedMessages[message] = timer;
}
internal void SwitchPlate(int idx, bool scrollTo = false) {
this._selectedPlate = idx;
this._scrollToSelected = scrollTo;
this._renameInput = string.Empty;
this._showRename = false;
this._deleteConfirm = false;
this._timedMessages.Clear();
this._shareTimer.Reset();
this.ResetEditing();
}
@ -883,19 +690,7 @@ namespace Glamaholic.Ui {
private void FilterItems(PlateSlot slot) {
var filter = this._itemFilter.ToLowerInvariant();
IEnumerable<Item> items;
if (GameFunctions.DresserContents.Count > 0 && this.Ui.Plugin.Config.ItemFilterShowObtainedOnly) {
var sheet = this.Ui.Plugin.DataManager.GetExcelSheet<Item>()!;
items = GameFunctions.DresserContents
.Select(item => sheet.GetRow(item.ItemId))
.Where(item => item != null)
.Cast<Item>();
} else {
items = this.Items;
}
this.FilteredItems = items
this.FilteredItems = this.Items
.Where(item => !Util.IsItemSkipped(item))
.Where(item => Util.MatchesSlot(item.EquipSlotCategory.Value!, slot))
.Where(item => this._itemFilter.Length == 0 || item.Name.RawString.ToLowerInvariant().Contains(filter))

View File

@ -1,6 +1,6 @@
using System;
using Dalamud.Game.Gui;
using Dalamud.Interface;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Component.GUI;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
@ -17,12 +17,12 @@ namespace Glamaholic {
return addon != null && addon->IsVisible;
}
private static unsafe bool IsOpen(IGameGui gui, string name) {
private static unsafe bool IsOpen(GameGui gui, string name) {
var addon = (AtkUnitBase*) gui.GetAddonByName(name, 1);
return IsOpen(addon);
}
internal static bool IsEditingPlate(IGameGui gui) {
internal static bool IsEditingPlate(GameGui gui) {
var plateOpen = IsOpen(gui, PlateAddon);
var boxOpen = IsOpen(gui, BoxAddon);
var armoireOpen = IsOpen(gui, ArmoireAddon);
@ -66,63 +66,6 @@ namespace Glamaholic {
ImGui.PopTextWrapPos();
}
internal static PlateSlot? GetSlot(Item item) {
var category = item.EquipSlotCategory.Value;
if (category == null) {
return null;
}
if (category.MainHand > 0) {
return PlateSlot.MainHand;
}
if (category.OffHand > 0) {
return PlateSlot.OffHand;
}
if (category.Head > 0) {
return PlateSlot.Head;
}
if (category.Body > 0) {
return PlateSlot.Body;
}
if (category.Gloves > 0) {
return PlateSlot.Hands;
}
if (category.Legs > 0) {
return PlateSlot.Legs;
}
if (category.Feet > 0) {
return PlateSlot.Feet;
}
if (category.Ears > 0) {
return PlateSlot.Ears;
}
if (category.Neck > 0) {
return PlateSlot.Neck;
}
if (category.Wrists > 0) {
return PlateSlot.Wrists;
}
if (category.FingerR > 0) {
return PlateSlot.RightRing;
}
if (category.FingerL > 0) {
return PlateSlot.LeftRing;
}
return null;
}
internal static bool MatchesSlot(EquipSlotCategory category, PlateSlot slot) {
return slot switch {
PlateSlot.MainHand => category.MainHand > 0,
@ -160,19 +103,5 @@ namespace Glamaholic {
_ => name.Length == 0 || name.StartsWith("Dated"),
};
}
internal static void TextIcon(FontAwesomeIcon icon) {
ImGui.PushFont(UiBuilder.IconFont);
ImGui.TextUnformatted(icon.ToIconString());
ImGui.PopFont();
}
internal static string GetClipboardText() {
try {
return ImGui.GetClipboardText();
} catch (Exception) {
return string.Empty;
}
}
}
}

View File

@ -10,14 +10,16 @@ plates into the plugin or apply new plates.
# Adding glamour plates
- From the main Glamaholic window, click "Plates" and then "New" to create an
empty glamour plate to edit from scratch.
Click on the Plates menu and you can add plates to Glamaholic.
- From the main Glamaholic window, click "Plates", hover over "Import", paste
a shared plate, then press Enter to import a shared plate.
- Click on the Glamaholic menu at the top left of the Glamour Plate Creation
window, then hover over "Create glamour plate" to import plates from the game.
- Click "New" to add an empty plate, starting from scratch.
- Hover over "Add from current plate", type in a name, and press Enter to copy a
plate from your glamour dresser into Glamaholic.
- Hover over "Import" and paste in a shared glamour plate to import a plate from
elsewhere.
- Open the Examine window a character, click the "Glamaholic" menu, then click
"Create glamour plate" to create a glamour plate from someone else's outfit.
@ -30,24 +32,3 @@ You can search for items that share the same model as another item by either
middle-clicking (click with mouse wheel) or holding Control and left-clicking on
any item name or item icon. This will open a window listing items with the same
model where you can link or try them on.
---
# Advanced search
The plate search box can be used to search for more than just plate names. Try
the options below.
- j:job can be used to search for a job by its abbreviation. Example: j:pld
- lvl:maxLevel can be used to search for plates equippable up to a specific
level. Example: lvl:1
- t:tag can be used to search by tag.
- !t:tag can be used to exclude tags.
- id:itemId can be used to search for plates with the given item ID.
- i:itemName can be used to search for plates with the given item name. Example:
i:skallic

View File

@ -1,28 +0,0 @@
{
"version": 1,
"dependencies": {
"net7.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.12, )",
"resolved": "2.1.12",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg=="
},
"Fody": {
"type": "Direct",
"requested": "[6.8.0, )",
"resolved": "6.8.0",
"contentHash": "hfZ/f8Mezt8aTkgv9nsvFdYoQ809/AqwsJlOGOPYIfBcG2aAIG3v3ex9d8ZqQuFYyMoucjRg4eKy3VleeGodKQ=="
},
"Resourcer.Fody": {
"type": "Direct",
"requested": "[1.8.1, )",
"resolved": "1.8.1",
"contentHash": "FPeK4jKyyX5+mIjTnHNReGZk2/2xDhmu44UsBI5w9WEhbr4oTMmht3rnBr46A+GCGepC4+2N41K4vExDYiGNVQ==",
"dependencies": {
"Fody": "6.6.4"
}
}
}
}
}

BIN
icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB