Add glamour sets collection helper

This commit is contained in:
Liza 2024-11-20 16:39:52 +01:00
parent e615cdfcd9
commit a9e7ff5cf2
Signed by: liza
GPG Key ID: 7199F8D727D55F67
4 changed files with 491 additions and 2 deletions

View File

@ -990,7 +990,7 @@ csharp_space_around_binary_operators = before_and_after
csharp_using_directive_placement = outside_namespace:silent csharp_using_directive_placement = outside_namespace:silent
csharp_prefer_simple_using_statement = true:suggestion csharp_prefer_simple_using_statement = true:suggestion
csharp_prefer_braces = true:silent 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_method_group_conversion = true:silent
csharp_style_prefer_top_level_statements = true:silent csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_primary_constructors = true:suggestion csharp_style_prefer_primary_constructors = true:suggestion

View File

@ -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<uint> MgpMakaiSets = new HashSet<uint>
{
// makai gear
45249, 45466, 45467, 45255, 45256, 45257, 45254, 45259, 45260, 45261, 45258, 45465, 45464, 45251, 45253,
45250, 45252
}.ToImmutableHashSet();
private static readonly ImmutableHashSet<uint> UndyedRathalosSets = new HashSet<uint>
{
45324, 45323
}.ToImmutableHashSet();
private static readonly ImmutableHashSet<uint> EternalBondingSets = new HashSet<uint>
{
45139, 45140, 45141, 45142, 45143, 45144
}.ToImmutableHashSet();
private static readonly ImmutableHashSet<uint> UnobtainableSets = new HashSet<uint>
{
// 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<InventoryType> _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<GlamourSet> _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<Cabinet>()
.Where(x => x.RowId > 0)
.Select(x => x.Item.RowId)
.ToHashSet();
var specialShopItems = dataManager.GetExcelSheet<SpecialShop>()
.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<Item> itemSheet = dataManager.GetExcelSheet<Item>();
_glamourSets = dataManager.GetExcelSheet<MirageStoreSetItem>()
.Where(x => x.RowId > 0)
.Select(x =>
{
var items = new List<uint>
{
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<GlamourSet> 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<GlamourSet> 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<GlamourSet> glamourSets, ESetType setType, List<GlamourSet> ownedSets,
HashSet<uint> 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<GlamourSet> glamourSets, List<GlamourSet> ownedSets,
HashSet<uint> 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<uint> GetOwnedItems()
{
HashSet<uint> ownedItems = new HashSet<uint>();
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<uint> 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<GlamourItem> 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,
}
}

View File

@ -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<CharacterData> 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<uint> GlamourDresserItems { get; set; } = [];
}
}

View File

@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using AutoRetainerAPI; using AutoRetainerAPI;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons; using ECommons;
@ -12,14 +13,22 @@ namespace KitchenSink;
[SuppressMessage("Performance", "CA1812")] [SuppressMessage("Performance", "CA1812")]
internal sealed class KitchenSinkPlugin : IDalamudPlugin internal sealed class KitchenSinkPlugin : IDalamudPlugin
{ {
private readonly IDalamudPluginInterface _pluginInterface;
private readonly WindowSystem _windowSystem = new(typeof(KitchenSinkPlugin).FullName);
private readonly AutoRetainerApi _autoRetainerApi; private readonly AutoRetainerApi _autoRetainerApi;
private readonly CharacterSwitch _characterSwitch; private readonly CharacterSwitch _characterSwitch;
private readonly DropboxQueue _dropboxQueue; private readonly DropboxQueue _dropboxQueue;
private readonly GlamourSetter _glamourSetter;
public KitchenSinkPlugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, public KitchenSinkPlugin(IDalamudPluginInterface pluginInterface, ICommandManager commandManager,
IClientState clientState, IChatGui chatGui, INotificationManager notificationManager, IDtrBar dtrBar, 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); DalamudReflector reflector = new DalamudReflector(pluginInterface, framework, pluginLog);
ECommonsMain.Init(pluginInterface, this); ECommonsMain.Init(pluginInterface, this);
@ -27,10 +36,18 @@ internal sealed class KitchenSinkPlugin : IDalamudPlugin
_characterSwitch = new CharacterSwitch(_autoRetainerApi, commandManager, clientState, chatGui, _characterSwitch = new CharacterSwitch(_autoRetainerApi, commandManager, clientState, chatGui,
notificationManager, dtrBar, condition, pluginLog); notificationManager, dtrBar, condition, pluginLog);
_dropboxQueue = new DropboxQueue(pluginInterface, reflector, commandManager, chatGui, 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() public void Dispose()
{ {
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_glamourSetter.Dispose();
_dropboxQueue.Dispose(); _dropboxQueue.Dispose();
_characterSwitch.Dispose(); _characterSwitch.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();