diff --git a/ARDiscard/ARDiscard.csproj b/ARDiscard/ARDiscard.csproj index 47b6ea2..d567f53 100644 --- a/ARDiscard/ARDiscard.csproj +++ b/ARDiscard/ARDiscard.csproj @@ -1,7 +1,7 @@ net7.0-windows - 2.0 + 2.1 11.0 enable true @@ -17,7 +17,7 @@ $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.1.1.1\ + $(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.1.1.4\ @@ -72,6 +72,7 @@ + diff --git a/ARDiscard/AutoDiscardPlogon.cs b/ARDiscard/AutoDiscardPlogon.cs index 8a0331f..c05513a 100644 --- a/ARDiscard/AutoDiscardPlogon.cs +++ b/ARDiscard/AutoDiscardPlogon.cs @@ -1,9 +1,13 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; +using ARDiscard.GameData; +using ARDiscard.Windows; using AutoRetainerAPI; using ClickLib.Clicks; using Dalamud.Data; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Command; using Dalamud.Game.Gui; using Dalamud.Interface.Windowing; @@ -19,11 +23,13 @@ using FFXIVClientStructs.FFXIV.Component.GUI; namespace ARDiscard; +[SuppressMessage("ReSharper", "UnusedType.Global")] public class AutoDiscardPlogon : IDalamudPlugin { private readonly WindowSystem _windowSystem = new(nameof(AutoDiscardPlogon)); private readonly Configuration _configuration; private readonly ConfigWindow _configWindow; + private readonly DiscardWindow _discardWindow; private readonly DalamudPluginInterface _pluginInterface; private readonly ChatGui _chatGui; @@ -36,26 +42,50 @@ public class AutoDiscardPlogon : IDalamudPlugin private DateTime _cancelDiscardAfter = DateTime.MaxValue; public AutoDiscardPlogon(DalamudPluginInterface pluginInterface, CommandManager commandManager, ChatGui chatGui, - DataManager dataManager, ClientState clientState) + DataManager dataManager, ClientState clientState, Condition condition) { + ItemCache itemCache = new ItemCache(dataManager); + _pluginInterface = pluginInterface; _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _chatGui = chatGui; _clientState = clientState; _commandManager = commandManager; - _commandManager.AddHandler("/discardconfig", new CommandInfo(OpenConfig)); - _commandManager.AddHandler("/discardall", new CommandInfo(ProcessCommand)); - _inventoryUtils = new(_configuration); + _commandManager.AddHandler("/discardconfig", new CommandInfo(OpenConfig) + { + HelpMessage = "Configures which items to automatically discard", + }); + _commandManager.AddHandler("/discardall", new CommandInfo(DiscardAll) + { + HelpMessage = "Discard all configured items now" + }); + _commandManager.AddHandler("/discard", new CommandInfo(OpenDiscardWindow) + { + HelpMessage = "Show what will be discarded with your current configuration", + }); + _inventoryUtils = new InventoryUtils(_configuration, itemCache); _pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; - _configWindow = new(_pluginInterface, _configuration, dataManager, clientState); + + _discardWindow = new(_inventoryUtils, itemCache, clientState, condition); + _windowSystem.AddWindow(_discardWindow); + + _configWindow = new(_pluginInterface, _configuration, itemCache, clientState, condition); _windowSystem.AddWindow(_configWindow); + _configWindow.DiscardNowClicked += (_, _) => OpenDiscardWindow(string.Empty, string.Empty); + _configWindow.ConfigSaved += (_, _) => _discardWindow.RefreshInventory(true); + _discardWindow.OpenConfigurationClicked += (_, _) => OpenConfigUi(); + _discardWindow.DiscardAllClicked += (_, filter) => + _taskManager!.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, filter)); + ECommonsMain.Init(_pluginInterface, this); _autoRetainerApi = new(); _taskManager = new(); + _clientState.Login += _discardWindow.Login; + _clientState.Logout += _discardWindow.Logout; _autoRetainerApi.OnRetainerPostprocessStep += CheckRetainerPostProcess; _autoRetainerApi.OnRetainerReadyToPostprocess += DoRetainerPostProcess; _autoRetainerApi.OnCharacterPostprocessStep += CheckCharacterPostProcess; @@ -80,7 +110,7 @@ public class AutoDiscardPlogon : IDalamudPlugin { PluginLog.Information($"Not running post-venture tasks for {name}, disabled for current character"); } - else if (_inventoryUtils.GetNextItemToDiscard() == null) + else if (_inventoryUtils.GetNextItemToDiscard(ItemFilter.None) == null) { PluginLog.Information($"Not running post-venture tasks for {name}, no items to discard"); } @@ -96,39 +126,41 @@ public class AutoDiscardPlogon : IDalamudPlugin private void DoRetainerPostProcess(string retainerName) { - _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer)); + _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer, ItemFilter.None)); } private void DoCharacterPostProcess() { - _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character)); + _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character, ItemFilter.None)); } private void OpenConfig(string command, string arguments) => OpenConfigUi(); private void OpenConfigUi() { - _configWindow.IsOpen = true; + _configWindow.IsOpen = !_configWindow.IsOpen; } - private void ProcessCommand(string command, string arguments) + private void DiscardAll(string command, string arguments) { - _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.FromCommand)); + _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, ItemFilter.None)); } - private unsafe void DiscardNextItem(PostProcessType type) + private void OpenDiscardWindow(string command, string arguments) + { + _discardWindow.IsOpen = !_discardWindow.IsOpen; + } + + private unsafe void DiscardNextItem(PostProcessType type, ItemFilter? itemFilter) { PluginLog.Information($"DiscardNextItem (type = {type})"); - InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(); + _discardWindow.Locked = true; + + InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter); if (nextItem == null) { PluginLog.Information($"No item to discard found"); - if (type == PostProcessType.Retainer) - _autoRetainerApi.FinishRetainerPostProcess(); - else if (type == PostProcessType.Character) - _autoRetainerApi.FinishCharacterPostProcess(); - else - _chatGui.Print("Done discarding."); + FinishDiscarding(type); } else { @@ -140,11 +172,12 @@ public class AutoDiscardPlogon : IDalamudPlugin _cancelDiscardAfter = DateTime.Now.AddSeconds(15); _taskManager.DelayNext(20); - _taskManager.Enqueue(() => ConfirmDiscardItem(type, inventoryType, slot)); + _taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot)); } } - private unsafe void ConfirmDiscardItem(PostProcessType type, InventoryType inventoryType, short slot) + private unsafe void ConfirmDiscardItem(PostProcessType type, ItemFilter? itemFilter, InventoryType inventoryType, + short slot) { var addon = GetDiscardAddon(); if (addon != null) @@ -154,83 +187,89 @@ public class AutoDiscardPlogon : IDalamudPlugin ClickSelectYesNo.Using((nint)addon).Yes(); _taskManager.DelayNext(20); - _taskManager.Enqueue(() => ContinueAfterDiscard(type, inventoryType, slot)); + _taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot)); } else { - InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(); + InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter); if (nextItem == null) { PluginLog.Information("Addon is not visible, but next item is also no longer set"); - if (type == PostProcessType.Retainer) - _autoRetainerApi.FinishRetainerPostProcess(); - else if (type == PostProcessType.Character) - _autoRetainerApi.FinishCharacterPostProcess(); - else - _chatGui.Print("Done discarding."); + FinishDiscarding(type); } else if (nextItem->Container == inventoryType && nextItem->Slot == slot) { PluginLog.Information( $"Addon is not (yet) visible, still trying to discard item in slot {slot} in inventory {inventoryType}"); _taskManager.DelayNext(100); - _taskManager.Enqueue(() => ConfirmDiscardItem(type, inventoryType, slot)); + _taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot)); } else { PluginLog.Information( $"Addon is not (yet) visible, but slot or inventory type changed, retrying from start"); _taskManager.DelayNext(100); - _taskManager.Enqueue(() => DiscardNextItem(type)); + _taskManager.Enqueue(() => DiscardNextItem(type, itemFilter)); } } } - private unsafe void ContinueAfterDiscard(PostProcessType type, InventoryType inventoryType, short slot) + private unsafe void ContinueAfterDiscard(PostProcessType type, ItemFilter? itemFilter, InventoryType inventoryType, + short slot) { - InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(); + InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter); if (nextItem == null) { PluginLog.Information($"Continuing after discard: no next item (type = {type})"); - if (type == PostProcessType.Retainer) - _autoRetainerApi.FinishRetainerPostProcess(); - else if (type == PostProcessType.Character) - _autoRetainerApi.FinishCharacterPostProcess(); - else - _chatGui.Print("Done discarding."); + FinishDiscarding(type); } else if (nextItem->Container == inventoryType && nextItem->Slot == slot) { if (_cancelDiscardAfter < DateTime.Now) { PluginLog.Information("No longer waiting for plugin to pop up, assume discard failed"); - if (type == PostProcessType.Retainer) - _autoRetainerApi.FinishRetainerPostProcess(); - else if (type == PostProcessType.Character) - _autoRetainerApi.FinishCharacterPostProcess(); - else - _chatGui.PrintError("Discarding probably failed due to an error."); + FinishDiscarding(type, "Discarding probably failed due to an error."); } else { PluginLog.Information($"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}"); _taskManager.DelayNext(20); - _taskManager.Enqueue(() => ContinueAfterDiscard(type, inventoryType, slot)); + _taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot)); } } else { PluginLog.Information($"ContinueAfterDiscard: Discovered different item to discard"); - _taskManager.EnqueueImmediate(() => DiscardNextItem(type)); + _taskManager.EnqueueImmediate(() => DiscardNextItem(type, itemFilter)); } } + private void FinishDiscarding(PostProcessType type, string? error = null) + { + if (type == PostProcessType.Retainer) + _autoRetainerApi.FinishRetainerPostProcess(); + else if (type == PostProcessType.Character) + _autoRetainerApi.FinishCharacterPostProcess(); + else + { + if (string.IsNullOrEmpty(error)) + _chatGui.Print("Done discarding."); + else + _chatGui.PrintError(error); + } + + _discardWindow.Locked = false; + _discardWindow.RefreshInventory(true); + } + public void Dispose() { _autoRetainerApi.OnRetainerPostprocessStep -= CheckRetainerPostProcess; _autoRetainerApi.OnRetainerReadyToPostprocess -= DoRetainerPostProcess; _autoRetainerApi.OnCharacterPostprocessStep -= CheckCharacterPostProcess; _autoRetainerApi.OnCharacterReadyToPostProcess -= DoCharacterPostProcess; + _clientState.Login -= _discardWindow.Login; + _clientState.Logout -= _discardWindow.Logout; _autoRetainerApi.Dispose(); ECommonsMain.Dispose(); @@ -238,6 +277,7 @@ public class AutoDiscardPlogon : IDalamudPlugin _inventoryUtils.Dispose(); _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; + _commandManager.RemoveHandler("/discard"); _commandManager.RemoveHandler("/discardall"); _commandManager.RemoveHandler("/discardconfig"); } @@ -274,6 +314,6 @@ public class AutoDiscardPlogon : IDalamudPlugin { Retainer, Character, - FromCommand, + ManuallyStarted, } } diff --git a/ARDiscard/Configuration.cs b/ARDiscard/Configuration.cs index 60ed387..ce7bdcd 100644 --- a/ARDiscard/Configuration.cs +++ b/ARDiscard/Configuration.cs @@ -3,7 +3,7 @@ using Dalamud.Configuration; namespace ARDiscard; -public class Configuration : IPluginConfiguration +public sealed class Configuration : IPluginConfiguration { public int Version { get; set; } = 1; public bool RunAfterVenture { get; set; } @@ -11,10 +11,20 @@ public class Configuration : IPluginConfiguration public List DiscardingItems { get; set; } = new(); public List ExcludedCharacters { get; set; } = new(); - public class CharacterInfo + public ArmouryConfiguration Armoury { get; set; } = new(); + + public sealed class CharacterInfo { public ulong LocalContentId { get; set; } public string CachedPlayerName { get; set; } public string CachedWorldName { get; set; } } + + public sealed class ArmouryConfiguration + { + public bool DiscardFromArmouryChest { get; set; } = false; + public bool CheckLeftSideGear { get; set; } = false; + public bool CheckRightSideGear { get; set; } = false; + public int MaximumGearItemLevel { get; set; } = 45; + } } diff --git a/ARDiscard/GameData/InternalConfiguration.cs b/ARDiscard/GameData/InternalConfiguration.cs new file mode 100644 index 0000000..37d3675 --- /dev/null +++ b/ARDiscard/GameData/InternalConfiguration.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; + +namespace ARDiscard.GameData; + +public static class InternalConfiguration +{ + public static IReadOnlyList BlacklistedItems = new List + { + 2820, // red onion helm + }.AsReadOnly(); +} diff --git a/ARDiscard/GameData/InventoryUtils.cs b/ARDiscard/GameData/InventoryUtils.cs new file mode 100644 index 0000000..c31ed1c --- /dev/null +++ b/ARDiscard/GameData/InventoryUtils.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Dalamud.Hooking; +using Dalamud.Logging; +using Dalamud.Utility.Signatures; +using FFXIVClientStructs.FFXIV.Client.Game; +using FFXIVClientStructs.FFXIV.Client.UI.Agent; +using FFXIVClientStructs.FFXIV.Client.UI.Misc; +using Lumina.Excel.GeneratedSheets; + +namespace ARDiscard.GameData; + +public class InventoryUtils : IDisposable +{ + private static readonly InventoryType[] DefaultInventoryTypes = + { + InventoryType.Inventory1, + InventoryType.Inventory2, + InventoryType.Inventory3, + InventoryType.Inventory4 + }; + + private static readonly InventoryType[] LeftSideGearInventoryTypes = + { + InventoryType.ArmoryHead, + InventoryType.ArmoryBody, + InventoryType.ArmoryHands, + InventoryType.ArmoryLegs, + InventoryType.ArmoryFeets + }; + + private static readonly InventoryType[] RightSideGearInventoryTypes = + { + InventoryType.ArmoryEar, + InventoryType.ArmoryNeck, + InventoryType.ArmoryHands, + InventoryType.ArmoryRings + }; + + private readonly Configuration _configuration; + private readonly ItemCache _itemCache; + + private unsafe delegate void DiscardItemDelegate(AgentInventoryContext* inventoryManager, InventoryItem* itemSlot, + InventoryType inventory, int slot, uint addonId, int position = -1); + + [Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 ?? 0F B7 48")] + private DiscardItemDelegate _discardItem = null!; + + public InventoryUtils(Configuration configuration, ItemCache itemCache) + { + _configuration = configuration; + _itemCache = itemCache; + SignatureHelper.Initialise(this); + } + + public void Dispose() + { + } + + public unsafe List GetAllItemsToDiscard() + { + List toDiscard = new List(); + + InventoryManager* inventoryManager = InventoryManager.Instance(); + foreach (InventoryType inventoryType in DefaultInventoryTypes) + toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, false)); + + if (_configuration.Armoury.DiscardFromArmouryChest) + { + if (_configuration.Armoury.CheckLeftSideGear) + { + foreach (InventoryType inventoryType in LeftSideGearInventoryTypes) + toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, true)); + } + + if (_configuration.Armoury.CheckRightSideGear) + { + foreach (InventoryType inventoryType in RightSideGearInventoryTypes) + toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, true)); + } + } + + return toDiscard; + } + + public unsafe InventoryItem* GetNextItemToDiscard(ItemFilter? itemFilter) + { + List allItemsToDiscard = GetAllItemsToDiscard(); + ItemWrapper? toDiscard = allItemsToDiscard.FirstOrDefault(x => + itemFilter == null || itemFilter.ItemIds.Contains(x.InventoryItem->ItemID)); + return toDiscard != null ? toDiscard.InventoryItem : null; + } + + private unsafe IReadOnlyList GetItemsToDiscard(InventoryManager* inventoryManager, + InventoryType inventoryType, bool doGearChecks) + { + List toDiscard = new List(); + + InventoryContainer* container = inventoryManager->GetInventoryContainer(inventoryType); + //PluginLog.Verbose($"Checking {inventoryType}, {container->Size}"); + for (int i = 0; i < container->Size; ++i) + { + var item = container->GetInventorySlot(i); + if (item != null) + { + if (InternalConfiguration.BlacklistedItems.Contains(item->ItemID)) + continue; + + if (doGearChecks) + { + if (IsItemPartOfGearset(item->ItemID)) + continue; + + ItemCache.CachedItemInfo? itemInfo = _itemCache.GetItem(item->ItemID); + if (itemInfo == null) + continue; // no info, who knows what that item is + + if (itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel) + continue; + } + + //PluginLog.Verbose($"{i} → {item->ItemID}"); + if (_configuration.DiscardingItems.Contains(item->ItemID)) + { + PluginLog.Information( + $"Found item {item->ItemID} to discard in inventory {inventoryType} in slot {i}"); + toDiscard.Add(new ItemWrapper { InventoryItem = item }); + } + } + else + { + //PluginLog.Verbose($"{i} → none"); + } + } + + return toDiscard; + } + + private unsafe bool IsItemPartOfGearset(uint searchForItemId) + { + var gearsetModule = RaptureGearsetModule.Instance(); + if (gearsetModule == null) + return true; // can't check gearsets, pretend everything is part of one + + for (int i = 0; i < 100; ++i) + { + var gearset = gearsetModule->GetGearset(i); + if (gearset != null && gearset->Flags.HasFlag(RaptureGearsetModule.GearsetFlag.Exists)) + { + var gearsetItems = new[] + { + gearset->MainHand, + gearset->OffHand, + gearset->Head, + gearset->Body, + gearset->Hands, + gearset->Legs, + gearset->Feet, + gearset->Ears, + gearset->Neck, + gearset->Wrists, + gearset->RingRight, + gearset->RightLeft, // why is this called RightLeft + }; + foreach (var gearsetItem in gearsetItems) + { + if (gearsetItem.ItemID == searchForItemId) + return true; + } + } + } + + return false; + } + + public unsafe void Discard(InventoryItem* item) + { + _discardItem(AgentInventoryContext.Instance(), item, item->Container, item->Slot, 0); + } + + public sealed unsafe class ItemWrapper + { + public required InventoryItem* InventoryItem { get; init; } + } +} diff --git a/ARDiscard/GameData/ItemCache.cs b/ARDiscard/GameData/ItemCache.cs new file mode 100644 index 0000000..eda64d1 --- /dev/null +++ b/ARDiscard/GameData/ItemCache.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace ARDiscard.GameData; + +public sealed class ItemCache +{ + private readonly Dictionary _items = new(); + + public ItemCache(DataManager dataManager) + { + foreach (var item in dataManager.GetExcelSheet()!) + { + if (item.RowId == 0) + continue; + + _items[item.RowId] = new CachedItemInfo + { + ItemId = item.RowId, + Name = item.Name.ToString(), + ILvl = item.LevelItem.Row, + Rarity = item.Rarity, + IsUnique = item.IsUnique, + IsUntradable = item.IsUntradable, + Level = item.LevelEquip, + UiCategory = item.ItemUICategory.Row, + }; + } + } + + public IEnumerable AllItems => _items.Values; + + public CachedItemInfo? GetItem(uint itemId) => _items[itemId]; + + public string GetItemName(uint itemId) + { + if (_items.TryGetValue(itemId, out var item)) + return item.Name; + return string.Empty; + } + + public sealed class CachedItemInfo + { + public required uint ItemId { get; init; } + public required string Name { get; init; } + public required uint ILvl { get; init; } + public required uint Level { get; init; } + public required byte Rarity { get; init; } + public required bool IsUnique { get; init; } + public required bool IsUntradable { get; init; } + public required uint UiCategory { get; init; } + } +} diff --git a/ARDiscard/GameData/ItemFilter.cs b/ARDiscard/GameData/ItemFilter.cs new file mode 100644 index 0000000..b71976a --- /dev/null +++ b/ARDiscard/GameData/ItemFilter.cs @@ -0,0 +1,10 @@ +using System.Collections.Generic; + +namespace ARDiscard.GameData; + +public class ItemFilter +{ + public static ItemFilter? None = null; + + public required List ItemIds { get; init; } +} diff --git a/ARDiscard/GameData/UiCategories.cs b/ARDiscard/GameData/UiCategories.cs new file mode 100644 index 0000000..2faf044 --- /dev/null +++ b/ARDiscard/GameData/UiCategories.cs @@ -0,0 +1,8 @@ +namespace ARDiscard.GameData; + +public static class UiCategories +{ + public const uint Unobtainable = 39; + public const uint Crystals = 59; + public const uint Currency = 100; +} diff --git a/ARDiscard/InventoryUtils.cs b/ARDiscard/InventoryUtils.cs deleted file mode 100644 index be6f99b..0000000 --- a/ARDiscard/InventoryUtils.cs +++ /dev/null @@ -1,67 +0,0 @@ -using System; -using Dalamud.Hooking; -using Dalamud.Logging; -using Dalamud.Utility.Signatures; -using FFXIVClientStructs.FFXIV.Client.Game; -using FFXIVClientStructs.FFXIV.Client.UI.Agent; - -namespace ARDiscard; - -public class InventoryUtils : IDisposable -{ - private readonly Configuration _configuration; - - private static readonly InventoryType[] InventoryTypes = - { InventoryType.Inventory1, InventoryType.Inventory2, InventoryType.Inventory3, InventoryType.Inventory4 }; - - private unsafe delegate void DiscardItemDelegate(AgentInventoryContext* inventoryManager, InventoryItem* itemSlot, - InventoryType inventory, int slot, uint addonId, int position = -1); - - [Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 ?? 0F B7 48")] - private DiscardItemDelegate _discardItem = null!; - - public InventoryUtils(Configuration configuration) - { - _configuration = configuration; - SignatureHelper.Initialise(this); - } - - public void Dispose() - { - } - - public unsafe InventoryItem* GetNextItemToDiscard() - { - InventoryManager* inventoryManager = InventoryManager.Instance(); - foreach (InventoryType inventoryType in InventoryTypes) - { - InventoryContainer* container = inventoryManager->GetInventoryContainer(inventoryType); - //PluginLog.Verbose($"Checking {inventoryType}, {container->Size}"); - for (int i = 0; i < container->Size; ++i) - { - var item = container->GetInventorySlot(i); - if (item != null) - { - //PluginLog.Verbose($"{i} → {item->ItemID}"); - if (_configuration.DiscardingItems.Contains(item->ItemID)) - { - PluginLog.Information( - $"Found item {item->ItemID} to discard in inventory {inventoryType} in slot {i}"); - return item; - } - } - else - { - //PluginLog.Verbose($"{i} → none"); - } - } - } - - return null; - } - - public unsafe void Discard(InventoryItem* item) - { - _discardItem(AgentInventoryContext.Instance(), item, item->Container, item->Slot, 0); - } -} diff --git a/ARDiscard/ConfigWindow.cs b/ARDiscard/Windows/ConfigWindow.cs similarity index 73% rename from ARDiscard/ConfigWindow.cs rename to ARDiscard/Windows/ConfigWindow.cs index 2956388..1d5e498 100644 --- a/ARDiscard/ConfigWindow.cs +++ b/ARDiscard/Windows/ConfigWindow.cs @@ -2,23 +2,26 @@ using System.Collections.Generic; using System.Linq; using System.Numerics; -using Dalamud.Data; +using ARDiscard.GameData; using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using ECommons; using ImGuiNET; -using Lumina.Excel.GeneratedSheets; +using Condition = Dalamud.Game.ClientState.Conditions.Condition; -namespace ARDiscard; +namespace ARDiscard.Windows; -public class ConfigWindow : Window +public sealed class ConfigWindow : Window { private readonly DalamudPluginInterface _pluginInterface; private readonly Configuration _configuration; - private readonly DataManager _dataManager; + private readonly ItemCache _itemCache; private readonly ClientState _clientState; + private readonly Condition _condition; private string _itemName = string.Empty; private List<(uint ItemId, string Name)> _searchResults = new(); @@ -26,14 +29,18 @@ public class ConfigWindow : Window private List<(uint ItemId, string Name)>? _allItems = null; private bool _resetKeyboardFocus = true; - public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, DataManager dataManager, - ClientState clientState) + public event EventHandler? DiscardNowClicked; + public event EventHandler? ConfigSaved; + + public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache, + ClientState clientState, Condition condition) : base("Auto Discard###AutoDiscardConfig") { _pluginInterface = pluginInterface; _configuration = configuration; - _dataManager = dataManager; + _itemCache = itemCache; _clientState = clientState; + _condition = condition; Size = new Vector2(600, 400); SizeCondition = ImGuiCond.FirstUseEver; @@ -45,7 +52,7 @@ public class ConfigWindow : Window }; _discarding.AddRange(_configuration.DiscardingItems - .Select(x => (x, dataManager.GetExcelSheet()?.GetRow(x)?.Name?.ToString() ?? x.ToString())).ToList()); + .Select(x => (x, itemCache.GetItemName(x))).ToList()); } public override void Draw() @@ -57,6 +64,13 @@ public class ConfigWindow : Window Save(); } + ImGui.SameLine(ImGui.GetWindowWidth() - 115 * ImGuiHelpers.GlobalScale); + ImGui.BeginDisabled(!_clientState.IsLoggedIn || !_condition[ConditionFlag.NormalConditions] || + DiscardNowClicked == null); + if (ImGui.Button("Preview Discards")) + DiscardNowClicked!.Invoke(this, EventArgs.Empty); + ImGui.EndDisabled(); + bool runBeforeLogout = _configuration.RunBeforeLogout; if (ImGui.Checkbox("[Global] Run before logging out in Multi-Mode", ref runBeforeLogout)) { @@ -68,6 +82,7 @@ public class ConfigWindow : Window { DrawDiscardList(); DrawExcludedCharacters(); + DrawExperimentalSettings(); ImGui.EndTabBar(); } @@ -170,7 +185,7 @@ public class ConfigWindow : Window { if (ImGui.BeginTabItem("Excluded Characters")) { - if (_clientState.IsLoggedIn && _clientState.LocalContentId > 0) + if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 }) { string worldName = _clientState.LocalPlayer?.HomeWorld.GameData?.Name ?? "??"; ImGui.TextWrapped( @@ -243,6 +258,49 @@ public class ConfigWindow : Window } } + private void DrawExperimentalSettings() + { + if (ImGui.BeginTabItem("Experimental Settings")) + { + bool discardFromArmouryChest = _configuration.Armoury.DiscardFromArmouryChest; + if (ImGui.Checkbox("Discard items from Armoury Chest", ref discardFromArmouryChest)) + { + _configuration.Armoury.DiscardFromArmouryChest = discardFromArmouryChest; + Save(); + } + + ImGui.BeginDisabled(!discardFromArmouryChest); + ImGui.Indent(30); + + bool leftSideGear = _configuration.Armoury.CheckLeftSideGear; + if (ImGui.Checkbox("Discard when items are found in Head/Body/Hands/Legs/Feet", ref leftSideGear)) + { + _configuration.Armoury.CheckLeftSideGear = leftSideGear; + Save(); + } + + bool rightSideGear = _configuration.Armoury.CheckRightSideGear; + if (ImGui.Checkbox("Discard when items are found in Accessories", ref rightSideGear)) + { + _configuration.Armoury.CheckRightSideGear = rightSideGear; + Save(); + } + + ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100); + int maximumItemLevel = _configuration.Armoury.MaximumGearItemLevel; + if (ImGui.InputInt("Ignore items >= this ilvl (Armoury Chest only)", + ref maximumItemLevel)) + { + _configuration.Armoury.MaximumGearItemLevel = Math.Max(0, Math.Min(625, maximumItemLevel)); + Save(); + } + + ImGui.Unindent(30); + ImGui.EndDisabled(); + ImGui.EndTabItem(); + } + } + private void UpdateResults() { if (string.IsNullOrEmpty(_itemName)) @@ -251,13 +309,11 @@ public class ConfigWindow : Window { if (_allItems == null) { - _allItems = _dataManager.GetExcelSheet()! - .Where(x => x.RowId != 0) + _allItems = _itemCache.AllItems .Where(x => !x.IsUnique && !x.IsUntradable) - .Where(x => x.ItemUICategory?.Value?.Name?.ToString() != "Currency" && - x.ItemUICategory?.Value?.Name?.ToString() != "Crystal") - .Where(x => !string.IsNullOrEmpty(x.Name.ToString())) - .Select(x => (x.RowId, x.Name.ToString())) + .Where(x => x.UiCategory != UiCategories.Currency && x.UiCategory != UiCategories.Crystals && + x.UiCategory != UiCategories.Unobtainable) + .Select(x => (x.ItemId, x.Name.ToString())) .ToList(); } @@ -272,5 +328,7 @@ public class ConfigWindow : Window { _configuration.DiscardingItems = _discarding.Select(x => x.ItemId).ToList(); _pluginInterface.SavePluginConfig(_configuration); + + ConfigSaved?.Invoke(this, EventArgs.Empty); } } diff --git a/ARDiscard/Windows/DiscardWindow.cs b/ARDiscard/Windows/DiscardWindow.cs new file mode 100644 index 0000000..ae3030d --- /dev/null +++ b/ARDiscard/Windows/DiscardWindow.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using ARDiscard.GameData; +using Dalamud.Game.ClientState; +using Dalamud.Game.ClientState.Conditions; +using Dalamud.Interface; +using Dalamud.Interface.Windowing; +using FFXIVClientStructs.FFXIV.Common.Math; +using ImGuiNET; + +namespace ARDiscard.Windows; + +public sealed class DiscardWindow : Window +{ + private readonly InventoryUtils _inventoryUtils; + private readonly ItemCache _itemCache; + private readonly ClientState _clientState; + private readonly Condition _condition; + + private List _displayedItems = new(); + + public event EventHandler? OpenConfigurationClicked; + public event EventHandler? DiscardAllClicked; + + public DiscardWindow(InventoryUtils inventoryUtils, ItemCache itemCache, ClientState clientState, + Condition condition) + : base("Discard Items") + { + _inventoryUtils = inventoryUtils; + _itemCache = itemCache; + _clientState = clientState; + _condition = condition; + + Size = new Vector2(600, 400); + SizeCondition = ImGuiCond.FirstUseEver; + + SizeConstraints = new WindowSizeConstraints + { + MinimumSize = new Vector2(600, 400), + MaximumSize = new Vector2(9999, 9999), + }; + } + + public bool Locked { get; set; } = false; + + public override void Draw() + { + ImGui.Text("With your current configuration, the following items would be discarded:"); + + ImGui.BeginDisabled(Locked); + if (ImGui.BeginChild("Right", new Vector2(-1, -30), true, ImGuiWindowFlags.NoSavedSettings)) + { + if (!_clientState.IsLoggedIn) + { + ImGui.Text("Not logged in."); + } + else if (_displayedItems.Count == 0) + { + ImGui.Text("No items to discard."); + } + else + { + foreach (var displayedItem in _displayedItems) + { + if (ImGui.Selectable(displayedItem.ToString(), displayedItem.Selected)) + displayedItem.Selected = !displayedItem.Selected; + } + } + } + + ImGui.EndDisabled(); + + ImGui.EndChild(); + + ImGui.BeginDisabled(OpenConfigurationClicked == null); + if (ImGui.Button("Open Configuration")) + OpenConfigurationClicked!.Invoke(this, EventArgs.Empty); + ImGui.EndDisabled(); + ImGui.SameLine(ImGui.GetWindowWidth() - 160 * ImGuiHelpers.GlobalScale); + ImGui.BeginDisabled(Locked || + !_clientState.IsLoggedIn || + !_condition[ConditionFlag.NormalConditions] || + _displayedItems.Count(x => x.Selected) == 0 || + DiscardAllClicked == null); + if (ImGui.Button("Discard all selected items")) + { + DiscardAllClicked!.Invoke(this, new ItemFilter + { + ItemIds = _displayedItems.Where(x => x.Selected).Select(x => x.ItemId).ToList() + }); + } + + ImGui.EndDisabled(); + } + + public override void OnOpen() => RefreshInventory(false); + + public override void OnClose() => _displayedItems.Clear(); + + public unsafe void RefreshInventory(bool keepSelected) + { + if (!IsOpen) + return; + + List notSelected = new(); + if (keepSelected) + { + notSelected.AddRange(_displayedItems + .Where(x => !x.Selected) + .Select(x => x.ItemId)); + } + + _displayedItems = _inventoryUtils.GetAllItemsToDiscard() + .GroupBy(x => x.InventoryItem->ItemID) + .Select(x => new SelectableItem + { + ItemId = x.Key, + Name = _itemCache.GetItemName(x.Key), + Quantity = x.Sum(y => y.InventoryItem->Quantity), + Selected = !notSelected.Contains(x.Key), + }) + .ToList(); + } + + private sealed class SelectableItem + { + public required uint ItemId { get; init; } + public required string Name { get; init; } + public required long Quantity { get; init; } + public bool Selected { get; set; } = true; + + public override string ToString() + { + if (Quantity > 1) + return $"{Name} ({Quantity}x)"; + + return Name; + } + } + + public void Login(object? sender, EventArgs e) => RefreshInventory(false); + + public void Logout(object? sender, EventArgs e) => _displayedItems.Clear(); +}