From a9e7ff5cf20004e99f9ed70319ac15013f09c735 Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Wed, 20 Nov 2024 16:39:52 +0100 Subject: [PATCH] Add glamour sets collection helper --- KitchenSink/.editorconfig | 2 +- KitchenSink/Commands/GlamourSetter.cs | 453 ++++++++++++++++++++++++++ KitchenSink/Configuration.cs | 19 ++ KitchenSink/KitchenSinkPlugin.cs | 19 +- 4 files changed, 491 insertions(+), 2 deletions(-) create mode 100644 KitchenSink/Commands/GlamourSetter.cs create mode 100644 KitchenSink/Configuration.cs diff --git a/KitchenSink/.editorconfig b/KitchenSink/.editorconfig index 6a4af82..cec12bf 100644 --- a/KitchenSink/.editorconfig +++ b/KitchenSink/.editorconfig @@ -990,7 +990,7 @@ csharp_space_around_binary_operators = before_and_after csharp_using_directive_placement = outside_namespace:silent csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_braces = true:silent -csharp_style_namespace_declarations = block_scoped:silent +csharp_style_namespace_declarations = file_scoped:silent csharp_style_prefer_method_group_conversion = true:silent csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_primary_constructors = true:suggestion diff --git a/KitchenSink/Commands/GlamourSetter.cs b/KitchenSink/Commands/GlamourSetter.cs new file mode 100644 index 0000000..af0aef2 --- /dev/null +++ b/KitchenSink/Commands/GlamourSetter.cs @@ -0,0 +1,453 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.ObjectModel; +using System.Linq; +using Dalamud.Game.Addon.Events; +using Dalamud.Game.Addon.Lifecycle; +using Dalamud.Game.Addon.Lifecycle.AddonArgTypes; +using Dalamud.Game.Command; +using Dalamud.Game.Text.SeStringHandling; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Style; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Interface.Windowing; +using Dalamud.Plugin; +using Dalamud.Plugin.Services; +using ECommons; +using FFXIVClientStructs.FFXIV.Client.Game; +using ImGuiNET; +using Lumina.Excel; +using Lumina.Excel.Sheets; + +namespace KitchenSink.Commands; + +internal sealed class GlamourSetter : Window, IDisposable +{ + private const uint ItemWolfMarks = 25; + private const uint ItemMgp = 29; + private const uint ItemTrophyCrystals = 36656; + + private static readonly ImmutableHashSet MgpMakaiSets = new HashSet + { + // makai gear + 45249, 45466, 45467, 45255, 45256, 45257, 45254, 45259, 45260, 45261, 45258, 45465, 45464, 45251, 45253, + 45250, 45252 + }.ToImmutableHashSet(); + + private static readonly ImmutableHashSet UndyedRathalosSets = new HashSet + { + 45324, 45323 + }.ToImmutableHashSet(); + + private static readonly ImmutableHashSet EternalBondingSets = new HashSet + { + 45139, 45140, 45141, 45142, 45143, 45144 + }.ToImmutableHashSet(); + + private static readonly ImmutableHashSet UnobtainableSets = new HashSet + { + // old pvp rewards + 45437, 45320, 45248, 45247, 45508, 45529, 45306, 45340, 45289, 45339, 45222, 45330, 45223, 45424, 45423 + }.ToImmutableHashSet(); + + private const uint MostRecentPvpSet = 45564; // loose fitting set + + private readonly List _inventoryTypes = + [ + InventoryType.Inventory1, + InventoryType.Inventory2, + InventoryType.Inventory3, + InventoryType.Inventory4, + InventoryType.SaddleBag1, + InventoryType.SaddleBag2, + InventoryType.PremiumSaddleBag1, + InventoryType.PremiumSaddleBag2, + InventoryType.EquippedItems, + InventoryType.ArmoryMainHand, + InventoryType.ArmoryOffHand, + InventoryType.ArmoryHead, + InventoryType.ArmoryBody, + InventoryType.ArmoryHands, + InventoryType.ArmoryLegs, + InventoryType.ArmoryFeets, + InventoryType.ArmoryEar, + InventoryType.ArmoryNeck, + InventoryType.ArmoryWrist, + InventoryType.ArmoryRings, + ]; + + private readonly IDalamudPluginInterface _pluginInterface; + private readonly IClientState _clientState; + private readonly IChatGui _chatGui; + private readonly ICommandManager _commandManager; + private readonly IAddonLifecycle _addonLifecycle; + private readonly Configuration _configuration; + private readonly ReadOnlyCollection _glamourSets; + private Configuration.CharacterData? _characterData; + + public GlamourSetter(IDalamudPluginInterface pluginInterface, IDataManager dataManager, IClientState clientState, + IChatGui chatGui, + ICommandManager commandManager, IAddonLifecycle addonLifecycle, Configuration configuration) + : base("Glamour Sets") + { + _pluginInterface = pluginInterface; + _clientState = clientState; + _chatGui = chatGui; + _commandManager = commandManager; + _addonLifecycle = addonLifecycle; + _configuration = configuration; + + + var armoireItems = dataManager.GetExcelSheet() + .Where(x => x.RowId > 0) + .Select(x => x.Item.RowId) + .ToHashSet(); + var specialShopItems = dataManager.GetExcelSheet() + .Where(x => x.RowId > 0 && !string.IsNullOrEmpty(x.Name.ToString())) + .SelectMany(x => x.Item.SelectMany(y => + y.ReceiveItems.Select(z => new SpecialShopItem + { + ItemId = z.Item.RowId, + CostItemId = y.ItemCosts[0].ItemCost.Value.RowId, + CostType = y.ItemCosts[0].ItemCost.Value.ItemUICategory.RowId, + CostName = y.ItemCosts[0].ItemCost.Value.Name.ToString(), + CostQuantity = y.ItemCosts[0].CurrencyCost, + }) + .Where(z => z.ItemId > 0 && (z.CostItemId < 100 || z.CostType == 100)))) + .GroupBy(x => x.ItemId) + .ToDictionary(x => x.Key, x => x.FirstOrDefault()); + + ExcelSheet itemSheet = dataManager.GetExcelSheet(); + _glamourSets = dataManager.GetExcelSheet() + .Where(x => x.RowId > 0) + .Select(x => + { + var items = new List + { + x.Unknown0, + x.Unknown1, + x.Unknown2, + x.Unknown3, + x.Unknown4, + x.Unknown5, + x.Unknown6, + x.Unknown7, + x.Unknown8, + x.Unknown9, + x.Unknown10 + } + .Where(y => y > 0) + .Select(y => itemSheet.GetRow(y)) + .Select(y => new GlamourItem + { + ItemId = y.RowId, + Name = y.Name.ToString(), + ShopItem = specialShopItems.GetValueOrDefault(y.RowId), + }) + .Where(y => !string.IsNullOrEmpty(y.Name)) + .ToList() + .AsReadOnly(); + + return new GlamourSet + { + ItemId = x.RowId, + Name = itemSheet.GetRow(x.RowId).Name.ToString(), + Items = items, + SetType = + x.RowId == MostRecentPvpSet + ? ESetType.PvP + : UnobtainableSets.Contains(x.RowId) + ? ESetType.Unobtainable + : EternalBondingSets.Contains(x.RowId) || UndyedRathalosSets.Contains(x.RowId) || MgpMakaiSets.Contains(x.RowId) + ? ESetType.Special + : items.FirstOrDefault()?.ShopItem?.CostItemId switch + { + ItemWolfMarks or ItemTrophyCrystals => ESetType.PvP, + ItemMgp => ESetType.MGP, + > 100 => ESetType.AlliedSociety, + _ => ESetType.Default, + }, + }; + }) + .Where(x => x.Items.Count > 0 && x.Items.Any(y => !armoireItems.Contains(y.ItemId))) + .OrderBy(x => x.Name) + .ThenBy(x => x.ItemId) + .ToList() + .AsReadOnly(); + + _commandManager.AddHandler("/glamoursets", new CommandInfo(ProcessCommand) + { + HelpMessage = "Shows the glamour set tracker" + }); + _addonLifecycle.RegisterListener(AddonEvent.PostRefresh, "MiragePrismPrismBox", UpdateFromGlamourDresser); + _clientState.Logout += Reset; + + if (_clientState.IsLoggedIn) + Update(); + } + + private void ProcessCommand(string command, string arguments) + { + IsOpen = !IsOpen; + } + + public override void PreOpenCheck() + { + ulong localContentId = _clientState.LocalContentId; + if (!_clientState.IsLoggedIn || localContentId == 0) + { + _characterData = null; + return; + } + + _characterData = _configuration.Characters.FirstOrDefault(x => localContentId == x.LocalContentId); + if (_characterData == null) + { + _characterData = new Configuration.CharacterData + { + LocalContentId = localContentId, + }; + _configuration.Characters.Add(_characterData); + _pluginInterface.SavePluginConfig(_configuration); + } + } + + public override void Draw() + { + if (_characterData == null) + { + ImGui.Text("You are not logged in."); + return; + } + + if (!_characterData.IsGlamourDresserInitialized) + { + ImGui.Text("Please access your glamour dresser."); + return; + } + + var ownedSets = _glamourSets.Where(x => _characterData.GlamourDresserItems.Contains(x.ItemId)).ToList(); + ImGui.Text( + $"Complete Sets: {ownedSets.Count} / {_glamourSets.Count(x => x.SetType != ESetType.Unobtainable || ownedSets.Contains(x))}"); + ImGui.Text($"Space saved: {ownedSets.Sum(x => x.Items.Count - 1)} items"); + + bool missingOnly = _configuration.ShowOnlyMissingGlamourSets; + if (ImGui.Checkbox("Show missing only", ref missingOnly)) + { + _configuration.ShowOnlyMissingGlamourSets = missingOnly; + _pluginInterface.SavePluginConfig(_configuration); + } + + ImGui.Separator(); + + using (var tabBar = ImRaii.TabBar("Tabs")) + { + if (tabBar) + { + DrawTab("Normal", ownedSets, ESetType.Default); + DrawTab("PvP", ownedSets, ESetType.PvP); + DrawTab("MGP", ownedSets, ESetType.MGP); + DrawTab("Allied Societies", ownedSets, ESetType.AlliedSociety); + DrawSpecialtyTab(ownedSets); + DrawTab("Unobtainable", ownedSets, ESetType.Unobtainable); + } + } + } + + private void DrawTab(string name, List ownedSets, ESetType setType) + { + using var tab = ImRaii.TabItem(name); + if (!tab) + return; + + var glamourSets = _glamourSets.Where(x => x.SetType == setType).ToList(); + if (_configuration.ShowOnlyMissingGlamourSets) + glamourSets = glamourSets.Except(ownedSets).ToList(); + + var ownedItems = GetOwnedItems(); + DrawMissingItemHeader(glamourSets, setType, ownedSets, ownedItems); + DrawSetRange(glamourSets, ownedSets, ownedItems); + } + + private void DrawSpecialtyTab(List ownedSets) + { + using var tab = ImRaii.TabItem("Special"); + if (!tab) + return; + + var glamourSets = _glamourSets.Where(x => x.SetType == ESetType.Special).ToList(); + if (_configuration.ShowOnlyMissingGlamourSets) + glamourSets = glamourSets.Except(ownedSets).ToList(); + + var ownedItems = GetOwnedItems(); + DrawMissingItemHeader(glamourSets, ESetType.Special, ownedSets, ownedItems); + if (ImGui.CollapsingHeader("Eternal Bonding")) + DrawSetRange(glamourSets.Where(x => EternalBondingSets.Contains(x.ItemId)).ToList(), ownedSets, ownedItems); + if (ImGui.CollapsingHeader("Makai Sets (MGP)")) + DrawSetRange(glamourSets.Where(x => MgpMakaiSets.Contains(x.ItemId)).ToList(), ownedSets, ownedItems); + if (ImGui.CollapsingHeader("Rathalos Sets (undyed)")) + DrawSetRange(glamourSets.Where(x => UndyedRathalosSets.Contains(x.ItemId)).ToList(), ownedSets, ownedItems); + } + + private static void DrawMissingItemHeader(List glamourSets, ESetType setType, List ownedSets, + HashSet ownedItems) + { + var missingItems = glamourSets + .Except(ownedSets) + .SelectMany(x => x.Items) + .Where(x => !ownedItems.Contains(x.ItemId)).ToList(); + if (setType == ESetType.PvP) + { + ImGui.Text( + $"Wolf Marks: {missingItems.Where(x => x is { ShopItem.CostItemId: ItemWolfMarks }).Sum(x => x.ShopItem!.CostQuantity):N0}"); + ImGui.Text( + $"Trophy Crystals: {missingItems.Where(x => x is { ShopItem.CostItemId: ItemTrophyCrystals }).Sum(x => x.ShopItem!.CostQuantity):N0}"); + ImGui.Separator(); + } + else if (setType is ESetType.MGP or ESetType.Special) + { + ImGui.Text( + $"MGP: {missingItems.Where(x => x is { ShopItem.CostItemId: ItemMgp }).Sum(x => x.ShopItem!.CostQuantity):N0}"); + ImGui.Separator(); + } + } + + private void DrawSetRange(List glamourSets, List ownedSets, + HashSet ownedItems) + { + foreach (var glamourSet in glamourSets) + { + if (ownedSets.Contains(glamourSet)) + ImGui.TextColored(ImGuiColors.ParsedGreen, glamourSet.Name); + else + { + int ownedCount = glamourSet.Items.Count(x => ownedItems.Contains(x.ItemId)); + if (ownedCount == glamourSet.Items.Count) + ImGui.TextColored(ImGuiColors.ParsedBlue, $"{glamourSet.Name} (Can be completed)"); + else if (ownedCount > 0) + ImGui.TextColored(ImGuiColors.DalamudYellow, glamourSet.Name); + else + ImGui.Text(glamourSet.Name); + + + using (ImRaii.PushIndent()) + { + foreach (var item in glamourSet.Items) + { + if (ownedItems.Contains(item.ItemId)) + ImGui.TextColored(ImGuiColors.ParsedGreen, item.Name); + else if (item.ShopItem is { } shopItem) + ImGui.Text($"{item.Name} ({shopItem.CostQuantity:N0}x {shopItem.CostName})"); + else + ImGui.Text(item.Name); + + if (ImGui.IsItemClicked()) + { + try + { + _chatGui.Print(SeString.CreateItemLink(item.ItemId, false)); + } + catch (Exception) + { + // doesn't matter, just nice-to-have + } + } + } + } + } + } + } + + private HashSet GetOwnedItems() + { + HashSet ownedItems = new HashSet(); + ownedItems.AddRange(_characterData?.GlamourDresserItems ?? []); + unsafe + { + InventoryManager* inventoryManager = InventoryManager.Instance(); + if (inventoryManager != null) + { + foreach (var inventoryType in _inventoryTypes) + { + var inventoryContainer = inventoryManager->GetInventoryContainer(inventoryType); + for (int i = 0; i < inventoryContainer->Size; ++i) + { + InventoryItem* item = inventoryContainer->GetInventorySlot(i); + if (item != null && item->ItemId != 0) + ownedItems.Add(item->ItemId % 1_000_000); + } + } + } + } + + return ownedItems; + } + + private unsafe void UpdateFromGlamourDresser(AddonEvent type, AddonArgs args) + { + if (_characterData == null) + return; + + MirageManager* mirageManager = MirageManager.Instance(); + if (mirageManager == null) + { + Reset(0, 0); + return; + } + + HashSet ownedItemIds = [..mirageManager->PrismBoxItemIds.ToArray().Select(x => x % 1_000_000)]; + if (_characterData.IsGlamourDresserInitialized || !_characterData.GlamourDresserItems.SetEquals(ownedItemIds)) + { + _characterData.IsGlamourDresserInitialized = true; + _characterData.GlamourDresserItems = ownedItemIds; + _pluginInterface.SavePluginConfig(_configuration); + } + } + + private void Reset(int type, int code) + { + _characterData = null; + } + + public void Dispose() + { + _clientState.Logout -= Reset; + _addonLifecycle.UnregisterListener(AddonEvent.PostRefresh, "MiragePrismPrismBox", UpdateFromGlamourDresser); + _commandManager.RemoveHandler("/glamoursets"); + } + + private sealed class GlamourSet + { + public required uint ItemId { get; init; } + public required string Name { get; init; } + public required ESetType SetType { get; init; } + public required IReadOnlyList Items { get; init; } + } + + private sealed class GlamourItem + { + public required uint ItemId { get; init; } + public required string Name { get; init; } + public required SpecialShopItem? ShopItem { get; init; } + } + + private sealed class SpecialShopItem + { + public required uint ItemId { get; init; } + public required uint CostItemId { get; init; } + public required uint CostType { get; init; } + public required string CostName { get; init; } + public required uint CostQuantity { get; init; } + } + + private enum ESetType + { + Default, + MGP, + PvP, + AlliedSociety, + Special, + Unobtainable, + } +} diff --git a/KitchenSink/Configuration.cs b/KitchenSink/Configuration.cs new file mode 100644 index 0000000..cbc7fee --- /dev/null +++ b/KitchenSink/Configuration.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Dalamud.Configuration; + +namespace KitchenSink; + +internal sealed class Configuration : IPluginConfiguration +{ + public int Version { get; set; } = 1; + public List Characters { get; set; } = []; + public bool ShowOnlyMissingGlamourSets { get; set; } = true; + + internal sealed class CharacterData + { + public ulong LocalContentId { get; set; } + + public bool IsGlamourDresserInitialized { get; set; } + public HashSet GlamourDresserItems { get; set; } = []; + } +} diff --git a/KitchenSink/KitchenSinkPlugin.cs b/KitchenSink/KitchenSinkPlugin.cs index 3775b94..93be08b 100644 --- a/KitchenSink/KitchenSinkPlugin.cs +++ b/KitchenSink/KitchenSinkPlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using AutoRetainerAPI; +using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ECommons; @@ -12,14 +13,22 @@ namespace KitchenSink; [SuppressMessage("Performance", "CA1812")] internal sealed class KitchenSinkPlugin : IDalamudPlugin { + private readonly IDalamudPluginInterface _pluginInterface; + private readonly WindowSystem _windowSystem = new(typeof(KitchenSinkPlugin).FullName); + private readonly AutoRetainerApi _autoRetainerApi; private readonly CharacterSwitch _characterSwitch; private readonly DropboxQueue _dropboxQueue; + private readonly GlamourSetter _glamourSetter; public KitchenSinkPlugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IClientState clientState, IChatGui chatGui, INotificationManager notificationManager, IDtrBar dtrBar, - ICondition condition, IFramework framework, IPluginLog pluginLog) + IDataManager dataManager, ICondition condition, IFramework framework, IAddonLifecycle addonLifecycle, + IPluginLog pluginLog) { + _pluginInterface = pluginInterface; + var configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); + DalamudReflector reflector = new DalamudReflector(pluginInterface, framework, pluginLog); ECommonsMain.Init(pluginInterface, this); @@ -27,10 +36,18 @@ internal sealed class KitchenSinkPlugin : IDalamudPlugin _characterSwitch = new CharacterSwitch(_autoRetainerApi, commandManager, clientState, chatGui, notificationManager, dtrBar, condition, pluginLog); _dropboxQueue = new DropboxQueue(pluginInterface, reflector, commandManager, chatGui, pluginLog); + _glamourSetter = new GlamourSetter(pluginInterface, dataManager, clientState, chatGui, commandManager, + addonLifecycle, configuration); + + _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; + _windowSystem.AddWindow(_glamourSetter); } public void Dispose() { + _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + + _glamourSetter.Dispose(); _dropboxQueue.Dispose(); _characterSwitch.Dispose(); _autoRetainerApi.Dispose();