Compare commits

..

32 Commits
v3.3 ... master

Author SHA1 Message Date
Liza cfbd064d4a
Automatically adjust max gear item level based on the highest ilvl green item (dungeon drop) 2024-07-19 19:20:31 +02:00
Liza d730e2c039
Fix confirm check 2024-07-05 21:48:21 +02:00
Liza ed2f8b9c86
API 10 2024-07-02 19:11:28 +02:00
Liza 33c9facad4
Replace deprecated gearset accessors 2024-06-26 01:54:12 +02:00
Liza 32775ac409
Allow auto-discarding irregular tomestones 2024-06-26 01:53:05 +02:00
Liza 51d94c923a
Ensure no two discard processes run at the same time 2024-04-20 22:16:25 +02:00
Liza 7396aa97d1
Changes for Patch 6.58 items 2024-04-14 16:23:51 +02:00
Liza e0e69e9b6a
Add sub parts to internal blacklist, add config for additional blacklisted items 2024-03-30 19:29:43 +01:00
Liza 3c986562b2
NET 8 2024-03-21 21:59:58 +01:00
Liza 1ee347f0ba
Add Sharlayan Cabinet/Wardrobe as discardable 2024-02-29 11:03:37 +01:00
Liza 56f6e949fc
Adjust log levels 2024-02-06 18:26:37 +01:00
Liza a94c5077fc
Allow gear coffers to be discarded 2024-02-04 09:38:58 +01:00
Liza 0d7f9f4aa4
Add an option to never discard items crafted manually 2024-02-03 21:14:16 +01:00
Liza 24b76b1b49
Fix not all armoury items being discarded 2024-01-25 09:48:50 +01:00
Liza 5db9c5e6fb
Enable context menu integration by default 2024-01-24 08:26:33 +01:00
Liza e419653744
Allow unapproved skybuilders' materials to be discarded 2024-01-24 08:21:05 +01:00
Liza 34b40a8c88
Allow for weapons/tools to be discarded 2024-01-24 07:55:49 +01:00
Liza f60cfab13b
Update icon URL 2024-01-16 06:51:38 +01:00
Liza c2d42317ce
Add IPC to retrieve items that'll be auto-discarded 2024-01-13 23:32:14 +01:00
Liza 3cce7bce2d
Add certain squadron-related stuff to discardable items 2023-12-22 17:57:38 +01:00
Liza b28367fb65
Add chat context menu entries 2023-12-22 17:50:49 +01:00
Liza 19e1e93416
Add ceruleum tanks/repair mats to blacklist 2023-12-22 17:40:22 +01:00
Liza b4bdad19f7
Add more fate drops as discardable items 2023-12-18 17:03:07 +01:00
Liza ce37130390
Refactoring, allow unique/untradeable items that can be purchased after completing quests to be discarded 2023-12-07 16:38:00 +01:00
Liza 5817f8c976
Fix wrong bracelets/hands inventory used during discards 2023-12-06 01:26:02 +01:00
Liza 239cb18968
Don't show gil/mgp/allagan tomestones in item search (although they wouldn't be discarded anyway) 2023-11-28 17:30:57 +01:00
Liza be97c709f8
Add normal raid drops as allowed discardable items 2023-11-27 20:35:07 +01:00
Liza 4ab8eb86a8
Bump version 2023-11-27 00:03:24 +01:00
Liza b86935dc56
Search: Show number of results, limit shown results due to performance 2023-11-27 00:02:28 +01:00
Liza 9bf199e91b
Add tribal quest fate drops as allowed discardable items 2023-11-26 23:54:45 +01:00
Liza 48c8052afe
Add antique gear (from Snowcloak/Sastasha Hard/Qarn Hard) to allowed discardable items 2023-11-26 13:30:11 +01:00
Liza 0d706ca395
Bump version/recompile 2023-11-14 20:08:42 +01:00
25 changed files with 2174 additions and 423 deletions

1017
ARDiscard/.editorconfig Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Dalamud.NET.Sdk/9.0.2">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net7.0-windows</TargetFramework> <Version>6.2</Version>
<Version>3.3</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<OutputPath>dist</OutputPath> <OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
<DebugType>portable</DebugType>
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <Import Project="..\LLib\LLib.targets"/>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath> <Import Project="..\LLib\RenameZip.targets"/>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj"/> <ProjectReference Include="..\AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj"/>
<ProjectReference Include="..\ECommons\ECommons\ECommons.csproj"/> <ProjectReference Include="..\ECommons\ECommons\ECommons.csproj"/>
<ProjectReference Include="..\LLib\LLib.csproj"/> <ProjectReference Include="..\LLib\LLib.csproj"/>
</ItemGroup> </ItemGroup>
<ItemGroup>
<PackageReference Include="Dalamud.ContextMenu" Version="1.3.1"/>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
</ItemGroup>
<ItemGroup>
<Reference Include="Dalamud">
<HintPath>$(DalamudLibPath)Dalamud.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="ImGui.NET">
<HintPath>$(DalamudLibPath)ImGui.NET.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina">
<HintPath>$(DalamudLibPath)Lumina.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Lumina.Excel">
<HintPath>$(DalamudLibPath)Lumina.Excel.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Newtonsoft.Json">
<HintPath>$(DalamudLibPath)Newtonsoft.Json.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="FFXIVClientStructs">
<HintPath>$(DalamudLibPath)FFXIVClientStructs.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project> </Project>

View File

@ -2,7 +2,13 @@
"Name": "Discard Helper", "Name": "Discard Helper",
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"Punchline": "Discard items automatically via a command or as a post-venture task for AutoRetainer", "Punchline": "Discard items automatically via a command or as a post-venture task for AutoRetainer",
"Description": "", "Description": "Small plugin to help keep your inventory organized (especially when automating retainers or submersibles) by discarding items you don't want (e.g. Stuffed Alphas)",
"RepoUrl": "https://git.carvel.li/liza/ARDiscard", "RepoUrl": "https://git.carvel.li/liza/ARDiscard",
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/ARDiscard.png" "IconUrl": "https://plugins.carvel.li/icons/ARDiscard.png",
"Tags": [
"autoretainer",
"automation",
"discard",
"yeet"
]
} }

View File

@ -1,6 +1,7 @@
using System; using System;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using ARDiscard.External;
using ARDiscard.GameData; using ARDiscard.GameData;
using ARDiscard.Windows; using ARDiscard.Windows;
using AutoRetainerAPI; using AutoRetainerAPI;
@ -10,7 +11,7 @@ using Dalamud.Memory;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons; using ECommons;
using ECommons.Automation; using ECommons.Automation.NeoTaskManager;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI; using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI; using FFXIVClientStructs.FFXIV.Component.GUI;
@ -19,14 +20,14 @@ using LLib;
namespace ARDiscard; namespace ARDiscard;
[SuppressMessage("ReSharper", "UnusedType.Global")] [SuppressMessage("ReSharper", "UnusedType.Global")]
public class AutoDiscardPlogon : IDalamudPlugin public sealed class AutoDiscardPlogon : IDalamudPlugin
{ {
private readonly WindowSystem _windowSystem = new(nameof(AutoDiscardPlogon)); private readonly WindowSystem _windowSystem = new(nameof(AutoDiscardPlogon));
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly DiscardWindow _discardWindow; private readonly DiscardWindow _discardWindow;
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
@ -34,20 +35,27 @@ public class AutoDiscardPlogon : IDalamudPlugin
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
private readonly InventoryUtils _inventoryUtils; private readonly InventoryUtils _inventoryUtils;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly GameStrings _gameStrings;
private readonly AutoRetainerApi _autoRetainerApi; private readonly AutoRetainerApi _autoRetainerApi;
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Obsolete in ECommons")]
private readonly TaskManager _taskManager; private readonly TaskManager _taskManager;
private readonly ContextMenuIntegration _contextMenuIntegration; private readonly ContextMenuIntegration _contextMenuIntegration;
private readonly AutoDiscardIpc _autoDiscardIpc;
private DateTime _cancelDiscardAfter = DateTime.MaxValue; private DateTime _cancelDiscardAfter = DateTime.MaxValue;
public AutoDiscardPlogon(DalamudPluginInterface pluginInterface, ICommandManager commandManager, IChatGui chatGui, [SuppressMessage("Maintainability", "CA1506")]
public AutoDiscardPlogon(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IChatGui chatGui,
IDataManager dataManager, IClientState clientState, ICondition condition, IPluginLog pluginLog, IDataManager dataManager, IClientState clientState, ICondition condition, IPluginLog pluginLog,
IGameGui gameGui, ITextureProvider textureProvider) IGameGui gameGui, ITextureProvider textureProvider, IContextMenu contextMenu)
{ {
ItemCache itemCache = new ItemCache(dataManager); ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
MigrateConfiguration(_configuration);
_chatGui = chatGui; _chatGui = chatGui;
_clientState = clientState; _clientState = clientState;
_pluginLog = pluginLog; _pluginLog = pluginLog;
@ -65,28 +73,39 @@ public class AutoDiscardPlogon : IDalamudPlugin
{ {
HelpMessage = "Show what will be discarded with your current configuration", HelpMessage = "Show what will be discarded with your current configuration",
}); });
_inventoryUtils = new InventoryUtils(_configuration, itemCache, _pluginLog);
ListManager listManager = new ListManager(_configuration);
ItemCache itemCache = new ItemCache(dataManager, listManager);
_inventoryUtils = new InventoryUtils(_configuration, itemCache, listManager, _pluginLog);
listManager.FinishInitialization();
_iconCache = new IconCache(textureProvider); _iconCache = new IconCache(textureProvider);
_gameStrings = new GameStrings(dataManager, pluginLog);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += OpenDiscardUi;
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi; _pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
_discardWindow = new(_inventoryUtils, itemCache, _iconCache, clientState, condition, _configuration); _discardWindow = new(_inventoryUtils, itemCache, _iconCache, clientState, condition, _configuration);
_windowSystem.AddWindow(_discardWindow); _windowSystem.AddWindow(_discardWindow);
_configWindow = new(_pluginInterface, _configuration, itemCache, clientState, condition); _configWindow = new(_pluginInterface, _configuration, itemCache, listManager, clientState, condition);
_windowSystem.AddWindow(_configWindow); _windowSystem.AddWindow(_configWindow);
_configWindow.DiscardNowClicked += (_, _) => OpenDiscardWindow(string.Empty, string.Empty); _configWindow.DiscardNowClicked += (_, _) => OpenDiscardWindow(string.Empty, string.Empty);
_configWindow.ConfigSaved += (_, _) => _discardWindow.RefreshInventory(true); _configWindow.ConfigSaved += (_, _) => _discardWindow.RefreshInventory(true);
_discardWindow.OpenConfigurationClicked += (_, _) => OpenConfigUi(); _discardWindow.OpenConfigurationClicked += (_, _) => OpenConfigUi();
_discardWindow.DiscardAllClicked += (_, filter) => _discardWindow.DiscardAllClicked += (_, filter) =>
_taskManager!.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, filter)); {
_taskManager?.Abort();
_taskManager?.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, filter));
};
ECommonsMain.Init(_pluginInterface, this); ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new(); _autoRetainerApi = new();
_taskManager = new(); _taskManager = new();
_contextMenuIntegration = new(_pluginInterface, _chatGui, itemCache, _configuration, _configWindow); _contextMenuIntegration = new(_chatGui, itemCache, _configuration, listManager, _configWindow, _gameGui, contextMenu);
_autoDiscardIpc = new(_pluginInterface, _configuration);
_clientState.Login += _discardWindow.Login; _clientState.Login += _discardWindow.Login;
_clientState.Logout += _discardWindow.Logout; _clientState.Logout += _discardWindow.Logout;
@ -96,6 +115,17 @@ public class AutoDiscardPlogon : IDalamudPlugin
_autoRetainerApi.OnCharacterReadyToPostProcess += DoCharacterPostProcess; _autoRetainerApi.OnCharacterReadyToPostProcess += DoCharacterPostProcess;
} }
private void MigrateConfiguration(Configuration configuration)
{
if (configuration.Version == 1)
{
configuration.ContextMenu.Enabled = true;
configuration.ContextMenu.OnlyWhenConfigIsOpen = false;
configuration.Version = 2;
_pluginInterface.SavePluginConfig(configuration);
}
}
private void CheckRetainerPostProcess(string retainerName) => private void CheckRetainerPostProcess(string retainerName) =>
CheckPostProcessInternal(PostProcessType.Retainer, retainerName, _configuration.RunAfterVenture); CheckPostProcessInternal(PostProcessType.Retainer, retainerName, _configuration.RunAfterVenture);
@ -128,11 +158,13 @@ public class AutoDiscardPlogon : IDalamudPlugin
private void DoRetainerPostProcess(string retainerName) private void DoRetainerPostProcess(string retainerName)
{ {
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer, ItemFilter.None)); _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer, ItemFilter.None));
} }
private void DoCharacterPostProcess() private void DoCharacterPostProcess()
{ {
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character, ItemFilter.None)); _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character, ItemFilter.None));
} }
@ -145,10 +177,13 @@ public class AutoDiscardPlogon : IDalamudPlugin
private void DiscardAll(string command, string arguments) private void DiscardAll(string command, string arguments)
{ {
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, ItemFilter.None)); _taskManager.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, ItemFilter.None));
} }
private void OpenDiscardWindow(string command, string arguments) private void OpenDiscardWindow(string command, string arguments) => OpenDiscardUi();
private void OpenDiscardUi()
{ {
_discardWindow.IsOpen = !_discardWindow.IsOpen; _discardWindow.IsOpen = !_discardWindow.IsOpen;
} }
@ -161,7 +196,7 @@ public class AutoDiscardPlogon : IDalamudPlugin
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter); InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
if (nextItem == null) if (nextItem == null)
{ {
_pluginLog.Information($"No item to discard found"); _pluginLog.Information("No item to discard found");
FinishDiscarding(type); FinishDiscarding(type);
} }
else else
@ -169,11 +204,11 @@ public class AutoDiscardPlogon : IDalamudPlugin
var (inventoryType, slot) = (nextItem->Container, nextItem->Slot); var (inventoryType, slot) = (nextItem->Container, nextItem->Slot);
_pluginLog.Information( _pluginLog.Information(
$"Discarding itemId {nextItem->ItemID} in slot {nextItem->Slot} of container {nextItem->Container}."); $"Discarding itemId {nextItem->ItemId} in slot {nextItem->Slot} of container {nextItem->Container}.");
_inventoryUtils.Discard(nextItem); _inventoryUtils.Discard(nextItem);
_cancelDiscardAfter = DateTime.Now.AddSeconds(15); _cancelDiscardAfter = DateTime.Now.AddSeconds(15);
_taskManager.DelayNext(20); _taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot)); _taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot));
} }
} }
@ -184,11 +219,11 @@ public class AutoDiscardPlogon : IDalamudPlugin
var addon = GetDiscardAddon(); var addon = GetDiscardAddon();
if (addon != null) if (addon != null)
{ {
_pluginLog.Information("Addon is visible, clicking 'yes'"); _pluginLog.Verbose("Addon is visible, clicking 'yes'");
((AddonSelectYesno*)addon)->YesButton->AtkComponentBase.SetEnabledState(true); ((AddonSelectYesno*)addon)->YesButton->AtkComponentBase.SetEnabledState(true);
addon->FireCallbackInt(0); addon->FireCallbackInt(0);
_taskManager.DelayNext(20); _taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot)); _taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot));
} }
else else
@ -203,14 +238,14 @@ public class AutoDiscardPlogon : IDalamudPlugin
{ {
_pluginLog.Information( _pluginLog.Information(
$"Addon is not (yet) visible, still trying to discard item in slot {slot} in inventory {inventoryType}"); $"Addon is not (yet) visible, still trying to discard item in slot {slot} in inventory {inventoryType}");
_taskManager.DelayNext(100); _taskManager.EnqueueDelay(100);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot)); _taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot));
} }
else else
{ {
_pluginLog.Information( _pluginLog.Information(
$"Addon is not (yet) visible, but slot or inventory type changed, retrying from start"); $"Addon is not (yet) visible, but slot or inventory type changed, retrying from start");
_taskManager.DelayNext(100); _taskManager.EnqueueDelay(100);
_taskManager.Enqueue(() => DiscardNextItem(type, itemFilter)); _taskManager.Enqueue(() => DiscardNextItem(type, itemFilter));
} }
} }
@ -234,16 +269,16 @@ public class AutoDiscardPlogon : IDalamudPlugin
} }
else else
{ {
_pluginLog.Information( _pluginLog.Verbose(
$"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}"); $"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}");
_taskManager.DelayNext(20); _taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot)); _taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot));
} }
} }
else else
{ {
_pluginLog.Information($"ContinueAfterDiscard: Discovered different item to discard"); _pluginLog.Information("ContinueAfterDiscard: Discovered different item to discard");
_taskManager.EnqueueImmediate(() => DiscardNextItem(type, itemFilter)); _taskManager.Enqueue(() => DiscardNextItem(type, itemFilter));
} }
} }
@ -274,12 +309,14 @@ public class AutoDiscardPlogon : IDalamudPlugin
_clientState.Login -= _discardWindow.Login; _clientState.Login -= _discardWindow.Login;
_clientState.Logout -= _discardWindow.Logout; _clientState.Logout -= _discardWindow.Logout;
_autoDiscardIpc.Dispose();
_contextMenuIntegration.Dispose(); _contextMenuIntegration.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();
ECommonsMain.Dispose(); ECommonsMain.Dispose();
_iconCache.Dispose(); _iconCache.Dispose();
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi; _pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
_pluginInterface.UiBuilder.OpenMainUi -= OpenDiscardUi;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_commandManager.RemoveHandler("/discard"); _commandManager.RemoveHandler("/discard");
_commandManager.RemoveHandler("/discardall"); _commandManager.RemoveHandler("/discardall");
@ -299,7 +336,7 @@ public class AutoDiscardPlogon : IDalamudPlugin
var textNode = addon->UldManager.NodeList[15]->GetAsAtkTextNode(); var textNode = addon->UldManager.NodeList[15]->GetAsAtkTextNode();
var text = MemoryHelper.ReadSeString(&textNode->NodeText).ExtractText(); var text = MemoryHelper.ReadSeString(&textNode->NodeText).ExtractText();
_pluginLog.Information($"YesNo prompt: {text}"); _pluginLog.Information($"YesNo prompt: {text}");
if (text.StartsWith("Discard")) if (_gameStrings.DiscardItem.IsMatch(text) || _gameStrings.DiscardCollectable.IsMatch(text))
{ {
return addon; return addon;
} }

View File

@ -5,16 +5,18 @@ namespace ARDiscard;
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; } = 2;
public bool RunAfterVenture { get; set; } public bool RunAfterVenture { get; set; }
public bool RunBeforeLogout { get; set; } public bool RunBeforeLogout { get; set; }
public List<uint> DiscardingItems { get; set; } = new(); public List<uint> DiscardingItems { get; set; } = new();
public List<uint> BlacklistedItems { get; set; } = new();
public List<CharacterInfo> ExcludedCharacters { get; set; } = new(); public List<CharacterInfo> ExcludedCharacters { get; set; } = new();
public ArmouryConfiguration Armoury { get; set; } = new(); public ArmouryConfiguration Armoury { get; set; } = new();
public ContextMenuConfiguration ContextMenu { get; set; } = new(); public ContextMenuConfiguration ContextMenu { get; set; } = new();
public PreviewConfiguration Preview { get; set; } = new(); public PreviewConfiguration Preview { get; set; } = new();
public uint IgnoreItemCountWhenAbove { get; set; } = 50; public uint IgnoreItemCountWhenAbove { get; set; } = 50;
public bool IgnoreItemWithSignature { get; set; }
public sealed class CharacterInfo public sealed class CharacterInfo
{ {
@ -25,16 +27,17 @@ internal sealed class Configuration : IPluginConfiguration
public sealed class ArmouryConfiguration public sealed class ArmouryConfiguration
{ {
public bool DiscardFromArmouryChest { get; set; } = false; public bool DiscardFromArmouryChest { get; set; }
public bool CheckLeftSideGear { get; set; } = false; public bool CheckMainHandOffHand { get; set; }
public bool CheckRightSideGear { get; set; } = false; public bool CheckLeftSideGear { get; set; }
public bool CheckRightSideGear { get; set; }
public int MaximumGearItemLevel { get; set; } = 45; public int MaximumGearItemLevel { get; set; } = 45;
} }
public sealed class ContextMenuConfiguration public sealed class ContextMenuConfiguration
{ {
public bool Enabled { get; set; } = false; public bool Enabled { get; set; } = true;
public bool OnlyWhenConfigIsOpen { get; set; } = true; public bool OnlyWhenConfigIsOpen { get; set; }
} }
public sealed class PreviewConfiguration public sealed class PreviewConfiguration

View File

@ -1,14 +1,11 @@
using System; using System;
using System.Linq;
using ARDiscard.GameData; using ARDiscard.GameData;
using ARDiscard.GameData.Agents;
using ARDiscard.Windows; using ARDiscard.Windows;
using Dalamud.ContextMenu; using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace ARDiscard; namespace ARDiscard;
@ -17,97 +14,151 @@ internal sealed class ContextMenuIntegration : IDisposable
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly ItemCache _itemCache; private readonly ItemCache _itemCache;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly IListManager _listManager;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly InventoryContextMenuItem _addItem; private readonly IGameGui _gameGui;
private readonly InventoryContextMenuItem _removeItem; private readonly IContextMenu _contextMenu;
private readonly DalamudContextMenu _dalamudContextMenu; private readonly MenuItem _addInventoryItem;
private readonly MenuItem _removeInventoryItem;
public ContextMenuIntegration(DalamudPluginInterface pluginInterface, IChatGui chatGui, ItemCache itemCache, public ContextMenuIntegration(IChatGui chatGui, ItemCache itemCache, Configuration configuration,
Configuration configuration, ConfigWindow configWindow) IListManager listManager, ConfigWindow configWindow, IGameGui gameGui, IContextMenu contextMenu)
{ {
_chatGui = chatGui; _chatGui = chatGui;
_itemCache = itemCache; _itemCache = itemCache;
_configuration = configuration; _configuration = configuration;
_listManager = listManager;
_configWindow = configWindow; _configWindow = configWindow;
_addItem = new InventoryContextMenuItem( _gameGui = gameGui;
new SeString(new UIForegroundPayload(52)) _contextMenu = contextMenu;
.Append($"\ue05f ") _addInventoryItem = new MenuItem
.Append(new UIForegroundPayload(0)).Append("Add to Auto Discard List"),
AddToDiscardList);
_removeItem = new InventoryContextMenuItem(
new SeString(new UIForegroundPayload(52))
.Append($"\ue05f ")
.Append(new UIForegroundPayload(0)).Append("Remove from Auto Discard List"),
RemoveFromDiscardList);
_dalamudContextMenu = new(pluginInterface);
_dalamudContextMenu.OnOpenInventoryContextMenu += OpenInventoryContextMenu;
}
private unsafe void OpenInventoryContextMenu(InventoryContextMenuOpenArgs args)
{
if (!_configuration.ContextMenu.Enabled)
return;
if (_configuration.ContextMenu.OnlyWhenConfigIsOpen && !_configWindow.IsOpen)
return;
if (args.ParentAddonName == "ArmouryBoard")
{ {
var agent = AgentModule.Instance()->GetAgentByInternalId(AgentId.ArmouryBoard); Prefix = (SeIconChar)57439,
if (agent == null || !agent->IsAgentActive()) PrefixColor = 52,
return; Name = "Add to Auto Discard List",
OnClicked = AddToDiscardList,
};
_removeInventoryItem = new MenuItem
{
Prefix = (SeIconChar)57439,
PrefixColor = 52,
Name = "Remove from Auto Discard List",
OnClicked = RemoveFromDiscardList,
};
// don't add it in the main/off hand weapon tabs, as we don't use these for discarding _contextMenu.OnMenuOpened += MenuOpened;
var agentArmouryBoard = (AgentArmouryBoard*)agent;
if (agentArmouryBoard->CurrentTab is 0 or 6)
return;
}
else if (!(args.ParentAddonName is "Inventory" or "InventoryExpansion" or "InventoryLarge"))
return;
if (!_configWindow.CanItemBeConfigured(args.ItemId))
return;
if (_configuration.DiscardingItems.Contains(args.ItemId))
args.AddCustomItem(_removeItem);
else if (!InternalConfiguration.BlacklistedItems.Contains(args.ItemId))
args.AddCustomItem(_addItem);
} }
private void AddToDiscardList(InventoryContextMenuItemSelectedArgs args) private void MenuOpened(IMenuOpenedArgs args)
{ {
if (_configWindow.AddToDiscardList(args.ItemId)) if (!IsEnabled())
return;
if (args.Target is MenuTargetInventory targetInventory)
{
if (args.AddonName is not ("Inventory" or "InventoryExpansion" or "InventoryLarge" or "ArmouryBoard") ||
targetInventory.TargetItem == null)
return;
var item = targetInventory.TargetItem.Value;
if (!_configWindow.CanItemBeConfigured(item.ItemId))
return;
if (_configuration.DiscardingItems.Contains(item.ItemId))
args.AddMenuItem(_removeInventoryItem);
else if (_itemCache.TryGetItem(item.ItemId, out ItemCache.CachedItemInfo? cachedItemInfo) &&
cachedItemInfo.CanBeDiscarded(_listManager))
args.AddMenuItem(_addInventoryItem);
}
else
{
if (args.AddonName is not "ChatLog")
return;
uint itemId = (uint)_gameGui.HoveredItem;
if (itemId > 1_000_000)
itemId -= 1_000_000;
if (itemId > 500_000)
itemId -= 500_000;
if (_configuration.DiscardingItems.Contains(itemId))
{
args.AddMenuItem(new MenuItem
{
Prefix = _removeInventoryItem.Prefix,
PrefixColor = _removeInventoryItem.PrefixColor,
Name = _removeInventoryItem.Name,
OnClicked = _ => RemoveFromDiscardList(itemId),
});
}
else if (_itemCache.TryGetItem(itemId, out ItemCache.CachedItemInfo? cachedItemInfo) &&
cachedItemInfo.CanBeDiscarded(_listManager))
{
args.AddMenuItem(new MenuItem
{
Prefix = _addInventoryItem.Prefix,
PrefixColor = _addInventoryItem.PrefixColor,
Name = _addInventoryItem.Name,
OnClicked = _ => AddToDiscardList(itemId),
});
}
}
}
private void AddToDiscardList(IMenuItemClickedArgs args) =>
AddToDiscardList(((MenuTargetInventory)args.Target).TargetItem!.Value.ItemId);
private void AddToDiscardList(uint itemId)
{
if (_configWindow.AddToDiscardList(itemId))
{ {
_chatGui.Print(new SeString(new UIForegroundPayload(52)) _chatGui.Print(new SeString(new UIForegroundPayload(52))
.Append($"\ue05f ") .Append($"\ue05f ")
.Append(new UIForegroundPayload(0)) .Append(new UIForegroundPayload(0))
.Append($"Added ") .Append($"Added ")
.Append(new UIForegroundPayload(52)) .Append(new UIForegroundPayload(52))
.Append(_itemCache.GetItemName(args.ItemId)) .Append(new ItemPayload(itemId))
.Append(_itemCache.GetItemName(itemId))
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0)) .Append(new UIForegroundPayload(0))
.Append(" to Auto Discard List.")); .Append(" to Auto Discard List."));
} }
} }
private void RemoveFromDiscardList(InventoryContextMenuItemSelectedArgs args) private void RemoveFromDiscardList(IMenuItemClickedArgs args) =>
RemoveFromDiscardList(((MenuTargetInventory)args.Target).TargetItem!.Value.ItemId);
private void RemoveFromDiscardList(uint itemId)
{ {
if (_configWindow.RemoveFromDiscardList(args.ItemId)) if (_configWindow.RemoveFromDiscardList(itemId))
{ {
_chatGui.Print(new SeString(new UIForegroundPayload(52)) _chatGui.Print(new SeString(new UIForegroundPayload(52))
.Append($"\ue05f ") .Append($"\ue05f ")
.Append(new UIForegroundPayload(0)) .Append(new UIForegroundPayload(0))
.Append($"Removed ") .Append($"Removed ")
.Append(new UIForegroundPayload(52)) .Append(new UIForegroundPayload(52))
.Append(_itemCache.GetItemName(args.ItemId)) .Append(new ItemPayload(itemId))
.Append(_itemCache.GetItemName(itemId))
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0)) .Append(new UIForegroundPayload(0))
.Append(" from Auto Discard List.")); .Append(" from Auto Discard List."));
} }
} }
private bool IsEnabled()
{
if (!_configuration.ContextMenu.Enabled)
return false;
if (_configuration.ContextMenu.OnlyWhenConfigIsOpen && !_configWindow.IsOpen)
return false;
return true;
}
public void Dispose() public void Dispose()
{ {
_dalamudContextMenu.OnOpenInventoryContextMenu -= OpenInventoryContextMenu; _contextMenu.OnMenuOpened -= MenuOpened;
_dalamudContextMenu.Dispose();
} }
} }

32
ARDiscard/External/AutoDiscardIpc.cs vendored Normal file
View File

@ -0,0 +1,32 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
namespace ARDiscard.External;
internal sealed class AutoDiscardIpc
{
private const string ItemsToDiscard = "ARDiscard.GetItemsToDiscard";
private readonly Configuration _configuration;
private readonly ICallGateProvider<IReadOnlySet<uint>> _getItemsToDiscard;
public AutoDiscardIpc(IDalamudPluginInterface pluginInterface, Configuration configuration)
{
_configuration = configuration;
_getItemsToDiscard = pluginInterface.GetIpcProvider<IReadOnlySet<uint>>(ItemsToDiscard);
_getItemsToDiscard.RegisterFunc(GetItemsToDiscard);
}
public void Dispose()
{
_getItemsToDiscard.UnregisterFunc();
}
private IReadOnlySet<uint> GetItemsToDiscard()
{
return _configuration.DiscardingItems.ToImmutableHashSet();
}
}

View File

@ -1,12 +0,0 @@
using System.Runtime.InteropServices;
using FFXIVClientStructs.Attributes;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
namespace ARDiscard.GameData.Agents;
[Agent(AgentId.ArmouryBoard)]
[StructLayout(LayoutKind.Explicit, Size = 0x2E)]
public struct AgentArmouryBoard
{
[FieldOffset(0x2C)] public byte CurrentTab;
}

View File

@ -0,0 +1,21 @@
using System.Data;
using System.Text.RegularExpressions;
using Dalamud.Plugin.Services;
using LLib;
using Lumina.Excel.GeneratedSheets;
namespace ARDiscard.GameData;
internal sealed class GameStrings
{
public GameStrings(IDataManager dataManager, IPluginLog pluginLog)
{
DiscardItem = dataManager.GetRegex<Addon>(110, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(DiscardItem)}");
DiscardCollectable = dataManager.GetRegex<Addon>(153, addon => addon.Text, pluginLog)
?? throw new ConstraintException($"Unable to resolve {nameof(DiscardCollectable)}");
}
public Regex DiscardItem { get; }
public Regex DiscardCollectable { get; }
}

View File

@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace ARDiscard.GameData;
internal interface IListManager
{
bool IsBlacklisted(uint itemId, bool checkConfiguration = true);
IReadOnlyList<uint> GetInternalBlacklist();
bool IsWhitelisted(uint itemId);
}

View File

@ -1,31 +0,0 @@
using System.Collections.Generic;
namespace ARDiscard.GameData;
internal static class InternalConfiguration
{
public static readonly IReadOnlyList<uint> BlacklistedItems = new List<uint>
{
2820, // red onion helm
16039, // ala mhigan earrings
24589, // aetheryte earrings
33648, // menphina's earrings
21197, // UCOB
23175, // UWU
28633, // TEA
36810, // DSR
38951, // TOP
}.AsReadOnly();
/// <summary>
/// Items that are unique/untradeable, but should still be possible to discard. This is moreso because
/// 99% of the unique/untradeable items should NOT be selectable for discard, but these are OK.
/// </summary>
public static readonly IReadOnlyList<uint> WhitelistedItems = new List<uint>
{
2962, // Onion Doublet
3279, // Onion Gaskins
3743, // Onion Patterns
}.AsReadOnly();
}

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
@ -11,38 +12,48 @@ namespace ARDiscard.GameData;
internal sealed class InventoryUtils internal sealed class InventoryUtils
{ {
private static readonly InventoryType[] DefaultInventoryTypes = private static readonly InventoryType[] DefaultInventoryTypes =
{ [
InventoryType.Inventory1, InventoryType.Inventory1,
InventoryType.Inventory2, InventoryType.Inventory2,
InventoryType.Inventory3, InventoryType.Inventory3,
InventoryType.Inventory4 InventoryType.Inventory4
}; ];
private static readonly InventoryType[] MainHandOffHandInventoryTypes =
[
InventoryType.ArmoryMainHand,
InventoryType.ArmoryOffHand
];
private static readonly InventoryType[] LeftSideGearInventoryTypes = private static readonly InventoryType[] LeftSideGearInventoryTypes =
{ [
InventoryType.ArmoryHead, InventoryType.ArmoryHead,
InventoryType.ArmoryBody, InventoryType.ArmoryBody,
InventoryType.ArmoryHands, InventoryType.ArmoryHands,
InventoryType.ArmoryLegs, InventoryType.ArmoryLegs,
InventoryType.ArmoryFeets InventoryType.ArmoryFeets
}; ];
private static readonly InventoryType[] RightSideGearInventoryTypes = private static readonly InventoryType[] RightSideGearInventoryTypes =
{ [
InventoryType.ArmoryEar, InventoryType.ArmoryEar,
InventoryType.ArmoryNeck, InventoryType.ArmoryNeck,
InventoryType.ArmoryHands, InventoryType.ArmoryWrist,
InventoryType.ArmoryRings InventoryType.ArmoryRings
}; ];
private static readonly IReadOnlyList<uint> NoGearsetItems = new List<uint>();
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ItemCache _itemCache; private readonly ItemCache _itemCache;
private readonly IListManager _listManager;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
public InventoryUtils(Configuration configuration, ItemCache itemCache, IPluginLog pluginLog) public InventoryUtils(Configuration configuration, ItemCache itemCache, IListManager listManager, IPluginLog pluginLog)
{ {
_configuration = configuration; _configuration = configuration;
_itemCache = itemCache; _itemCache = itemCache;
_listManager = listManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
} }
@ -53,42 +64,47 @@ internal sealed class InventoryUtils
InventoryManager* inventoryManager = InventoryManager.Instance(); InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (InventoryType inventoryType in DefaultInventoryTypes) foreach (InventoryType inventoryType in DefaultInventoryTypes)
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, false, null)); toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, NoGearsetItems));
if (_configuration.Armoury.DiscardFromArmouryChest) if (_configuration.Armoury.DiscardFromArmouryChest)
{ {
var gearsetItems = GetAllGearsetItems(); var gearsetItems = GetAllGearsetItems();
toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckMainHandOffHand, inventoryManager,
if (_configuration.Armoury.CheckLeftSideGear) MainHandOffHandInventoryTypes, itemCounts, gearsetItems));
{ toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckLeftSideGear, inventoryManager,
foreach (InventoryType inventoryType in LeftSideGearInventoryTypes) LeftSideGearInventoryTypes, itemCounts, gearsetItems));
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, true, toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckRightSideGear, inventoryManager,
gearsetItems)); RightSideGearInventoryTypes, itemCounts, gearsetItems));
}
if (_configuration.Armoury.CheckRightSideGear)
{
foreach (InventoryType inventoryType in RightSideGearInventoryTypes)
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, true,
gearsetItems));
}
} }
return toDiscard return toDiscard
.Where(x => itemCounts[x.InventoryItem->ItemID] < _configuration.IgnoreItemCountWhenAbove) .Where(x => itemCounts[x.InventoryItem->ItemId] < _configuration.IgnoreItemCountWhenAbove)
.ToList(); .ToList();
} }
private unsafe ReadOnlyCollection<ItemWrapper> GetArmouryItemsToDiscard(bool condition, InventoryManager* inventoryManager,
InventoryType[] inventoryTypes, Dictionary<uint, uint> itemCounts, List<uint>? gearsetItems)
{
List<ItemWrapper> items = new();
if (condition)
{
foreach (InventoryType inventoryType in inventoryTypes)
items.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, gearsetItems));
}
return items.AsReadOnly();
}
public unsafe InventoryItem* GetNextItemToDiscard(ItemFilter? itemFilter) public unsafe InventoryItem* GetNextItemToDiscard(ItemFilter? itemFilter)
{ {
List<ItemWrapper> allItemsToDiscard = GetAllItemsToDiscard(); List<ItemWrapper> allItemsToDiscard = GetAllItemsToDiscard();
ItemWrapper? toDiscard = allItemsToDiscard.FirstOrDefault(x => ItemWrapper? toDiscard = allItemsToDiscard.FirstOrDefault(x =>
itemFilter == null || itemFilter.ItemIds.Contains(x.InventoryItem->ItemID)); itemFilter == null || itemFilter.ItemIds.Contains(x.InventoryItem->ItemId));
return toDiscard != null ? toDiscard.InventoryItem : null; return toDiscard != null ? toDiscard.InventoryItem : null;
} }
private unsafe IReadOnlyList<ItemWrapper> GetItemsToDiscard(InventoryManager* inventoryManager, private unsafe ReadOnlyCollection<ItemWrapper> GetItemsToDiscard(InventoryManager* inventoryManager,
InventoryType inventoryType, Dictionary<uint, uint> itemCounts, bool doGearChecks, InventoryType inventoryType, Dictionary<uint, uint> itemCounts,
IReadOnlyList<uint>? gearsetItems) IReadOnlyList<uint>? gearsetItems)
{ {
List<ItemWrapper> toDiscard = new List<ItemWrapper>(); List<ItemWrapper> toDiscard = new List<ItemWrapper>();
@ -97,34 +113,36 @@ internal sealed class InventoryUtils
for (int i = 0; i < container->Size; ++i) for (int i = 0; i < container->Size; ++i)
{ {
var item = container->GetInventorySlot(i); var item = container->GetInventorySlot(i);
if (item != null && item->ItemID != 0) if (item != null && item->ItemId != 0)
{ {
if (itemCounts.TryGetValue(item->ItemID, out uint itemCount)) if (itemCounts.TryGetValue(item->ItemId, out uint itemCount))
itemCounts[item->ItemID] = itemCount + item->Quantity; itemCounts[item->ItemId] = itemCount + item->Quantity;
else else
itemCounts[item->ItemID] = item->Quantity; itemCounts[item->ItemId] = item->Quantity;
if (InternalConfiguration.BlacklistedItems.Contains(item->ItemID)) if (_listManager.IsBlacklisted(item->ItemId))
continue; continue;
if (doGearChecks) if (!_itemCache.TryGetItem(item->ItemId, out ItemCache.CachedItemInfo? itemInfo) ||
{ !itemInfo.CanBeDiscarded(_listManager))
if (gearsetItems == null || gearsetItems.Contains(item->ItemID)) continue; // no info, who knows what that item is
continue;
ItemCache.CachedItemInfo? itemInfo = _itemCache.GetItem(item->ItemID); // skip gear if we're unable to load gearsets or it is used in a gearset
if (itemInfo == null) if (itemInfo.EquipSlotCategory > 0 && (gearsetItems == null || gearsetItems.Contains(item->ItemId)))
continue; // no info, who knows what that item is continue;
if (itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel) if (itemInfo is { EquipSlotCategory: > 0, CanBeBoughtFromCalamitySalvager: false } &&
continue; itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel)
} continue;
if (_configuration.IgnoreItemWithSignature && item->CrafterContentId != 0)
continue;
//PluginLog.Verbose($"{i} → {item->ItemID}"); //PluginLog.Verbose($"{i} → {item->ItemID}");
if (_configuration.DiscardingItems.Contains(item->ItemID)) if (_configuration.DiscardingItems.Contains(item->ItemId))
{ {
_pluginLog.Information( _pluginLog.Verbose(
$"Found item {item->ItemID} to discard in inventory {inventoryType} in slot {i}"); $"Found item {item->ItemId} to discard in inventory {inventoryType} in slot {i}");
toDiscard.Add(new ItemWrapper { InventoryItem = item }); toDiscard.Add(new ItemWrapper { InventoryItem = item });
} }
} }
@ -134,7 +152,7 @@ internal sealed class InventoryUtils
} }
} }
return toDiscard; return toDiscard.AsReadOnly();
} }
private unsafe List<uint>? GetAllGearsetItems() private unsafe List<uint>? GetAllGearsetItems()
@ -151,23 +169,23 @@ internal sealed class InventoryUtils
{ {
var gearsetItems = new[] var gearsetItems = new[]
{ {
gearset->MainHand, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.MainHand),
gearset->OffHand, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.OffHand),
gearset->Head, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Head),
gearset->Body, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Body),
gearset->Hands, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Hands),
gearset->Legs, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Legs),
gearset->Feet, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Feet),
gearset->Ears, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Ears),
gearset->Neck, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Neck),
gearset->Wrists, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Wrists),
gearset->RingRight, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.RingLeft),
gearset->RingLeft, gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.RingRight),
}; };
foreach (var gearsetItem in gearsetItems) foreach (var gearsetItem in gearsetItems)
{ {
if (gearsetItem.ItemID != 0) if (gearsetItem.ItemId != 0)
allGearsetItems.Add(gearsetItem.ItemID); allGearsetItems.Add(gearsetItem.ItemId);
} }
} }
} }
@ -177,8 +195,8 @@ internal sealed class InventoryUtils
public unsafe void Discard(InventoryItem* item) public unsafe void Discard(InventoryItem* item)
{ {
if (InternalConfiguration.BlacklistedItems.Contains(item->ItemID)) if (_listManager.IsBlacklisted(item->ItemId))
throw new Exception($"Can't discard {item->ItemID}"); throw new ArgumentException($"Can't discard {item->ItemId}", nameof(item));
AgentInventoryContext.Instance()->DiscardItem(item, item->Container, item->Slot, 0); AgentInventoryContext.Instance()->DiscardItem(item, item->Container, item->Slot, 0);
} }

View File

@ -1,5 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets; using Lumina.Excel.GeneratedSheets;
namespace ARDiscard.GameData; namespace ARDiscard.GameData;
@ -8,7 +11,7 @@ internal sealed class ItemCache
{ {
private readonly Dictionary<uint, CachedItemInfo> _items = new(); private readonly Dictionary<uint, CachedItemInfo> _items = new();
public ItemCache(IDataManager dataManager) public ItemCache(IDataManager dataManager, ListManager listManager)
{ {
foreach (var item in dataManager.GetExcelSheet<Item>()!) foreach (var item in dataManager.GetExcelSheet<Item>()!)
{ {
@ -28,12 +31,68 @@ internal sealed class ItemCache
Level = item.LevelEquip, Level = item.LevelEquip,
UiCategory = item.ItemUICategory.Row, UiCategory = item.ItemUICategory.Row,
UiCategoryName = item.ItemUICategory.Value!.Name.ToString(), UiCategoryName = item.ItemUICategory.Value!.Name.ToString(),
EquipSlotCategory = item.EquipSlotCategory.Row,
}; };
if (item is { Rarity: 3, MateriaSlotCount: 3, RowId: < 33154 or > 33358 })
listManager.AddToInternalBlacklist(item.RowId);
if (item is { ItemSearchCategory.Row: 79, ItemUICategory.Row: >= 101 and <= 104 })
listManager.AddToInternalBlacklist(item.RowId);
} }
foreach (var shopItem in dataManager.GetExcelSheet<GilShopItem>()!)
{
// exclude base ARR relics, not strictly necessary since we don't allow discarding weapons anyway
if (shopItem.Item.Value!.Rarity == 4)
continue;
// the item can be discarded already
if (!_items.TryGetValue(shopItem.Item.Row, out CachedItemInfo? cachedItemInfo) ||
cachedItemInfo.CanBeDiscarded(listManager))
continue;
if (shopItem.AchievementRequired.Row != 0)
continue;
// has a quest required to unlock from the shop
if (!shopItem.QuestRequired.Any(CanDiscardItemsFromQuest))
continue;
cachedItemInfo.CanBeBoughtFromCalamitySalvager = true;
}
// only look at msq + regional side quests
foreach (var quest in dataManager.GetExcelSheet<Quest>()!.Where(x => x.JournalGenre.Value?.JournalCategory.Value?.JournalSection.Row is 0 or 1 or 3))
{
foreach (var itemId in quest.ItemReward.Where(x => x > 0))
{
var item = dataManager.GetExcelSheet<Item>()!.GetRow(itemId);
if (item is { Rarity: 1, ItemAction.Row: 388 } && item.RowId != 38809 && item.RowId != 29679)
listManager.AddToInternalWhitelist(item.RowId);
}
}
MaxDungeonItemLevel = _items.Values.Where(x => x.Rarity == 2)
.Select(x => (int)x.ILvl)
.Max();
}
public int MaxDungeonItemLevel { get; }
private bool CanDiscardItemsFromQuest(LazyRow<Quest> quest)
{
return quest.Row > 0 &&
quest.Value?.JournalGenre.Value?.JournalCategory.Value?.JournalSection
.Row is 0 or 1 or 6; // pre-EW MSQ, EW MSQ or Job/Class quest
} }
public IEnumerable<CachedItemInfo> AllItems => _items.Values; public IEnumerable<CachedItemInfo> AllItems => _items.Values;
public bool TryGetItem(uint itemId, [NotNullWhen(true)] out CachedItemInfo? item)
=> _items.TryGetValue(itemId, out item);
public CachedItemInfo? GetItem(uint itemId) public CachedItemInfo? GetItem(uint itemId)
{ {
if (_items.TryGetValue(itemId, out var item)) if (_items.TryGetValue(itemId, out var item))
@ -71,7 +130,25 @@ internal sealed class ItemCache
/// </summary> /// </summary>
public required bool IsIndisposable { get; init; } public required bool IsIndisposable { get; init; }
public bool CanBeBoughtFromCalamitySalvager { get; set; }
public required uint UiCategory { get; init; } public required uint UiCategory { get; init; }
public required string UiCategoryName { get; init; } public required string UiCategoryName { get; init; }
public required uint EquipSlotCategory { get; init; }
public bool CanBeDiscarded(IListManager listManager, bool checkConfiguration = true)
{
if (listManager.IsBlacklisted(ItemId, checkConfiguration))
return false;
if (UiCategory is UiCategories.Currency or UiCategories.Crystals or UiCategories.Unobtainable)
return false;
if (listManager.IsWhitelisted(ItemId))
return true;
return CanBeBoughtFromCalamitySalvager ||
this is { IsUnique: false, IsUntradable: false, IsIndisposable: false };
}
} }
} }

View File

@ -2,9 +2,9 @@
namespace ARDiscard.GameData; namespace ARDiscard.GameData;
internal class ItemFilter internal sealed class ItemFilter
{ {
public static ItemFilter? None = null; public const ItemFilter? None = null;
public required List<uint> ItemIds { get; init; } public required List<uint> ItemIds { get; init; }
} }

View File

@ -0,0 +1,322 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
namespace ARDiscard.GameData;
internal sealed class ListManager : IListManager
{
/// <summary>
/// Not all of these *can* be discarded, but we shouldn't attempt it either.
/// </summary>
private ISet<uint> _blacklistedItems = new List<uint>
{
2820, // red onion helm
16039, // ala mhigan earrings
24589, // aetheryte earrings
33648, // menphina's earrings
41081, // azeyma's earrings
10155, // Ceruleum Tank
10373, // Magitek Repair Materials
21197, // UCOB token
23175, // UWU token
28633, // TEA token
36810, // DSR token
38951, // TOP token
}
.Concat(Enumerable.Range(1, 99).Select(x => (uint)x))
.ToHashSet();
/// <summary>
/// Items that are unique/untradeable, but should still be possible to discard. This is moreso because
/// 99% of the unique/untradeable items should NOT be selectable for discard, but these are OK.
/// </summary>
private ISet<uint> _whitelistedItems = new HashSet<uint>()
{
2962, // Onion Doublet
3279, // Onion Gaskins
3743, // Onion Patterns
9387, // Antique Helm
9388, // Antique Mail
9389, // Antique Gauntlets
9390, // Antique Breeches
9391, // Antique Sollerets
6223, // Mended Imperial Pot Helm
6224, // Mended Imperial Short Robe
7060, // Durability Draught
14945, // Squadron Enlistment Manual
15772, // Contemporary Warfare: Defense
15773, // Contemporary Warfare: Offense
15774, // Contemporary Warfare: Magicks
4572, // Company-issue Tonic
20790, // High Grade Company-issue Tonic
#region Fate drops used in tribal quests
7001, // Flamefang Choker (Amalj'aa)
7002, // Proto Armor Identification Key (Sylphs)
7003, // Kafre's Tusk (Sylphs)
7797, // Titan Mythrilshield (Kobolds)
7798, // Coral Armband (Sahagin)
7802, // Titan Steelshield (Kobolds)
7803, // Titan Electrumshield (Kobolds)
7804, // Coral Band (Sahagin)
7805, // Coral Necklace (Sahagin)
#endregion
#region Fate drops that can be turned in for mount/glamour/whatever
12252, // Coeurlregina Horn
12253, // Proto Ultima Exoplating
18032, // Stygian Ash
20521, // Sassho-seki Fragment
20638, // Ixion Horn
27972, // Archaeotania's Horn
27973, // Formidable Cog
36633, // Chi Bolt
36634, // Daivadipa's Bead
#endregion
#region Normal Raid trash (with no weekly lockout)
// A1-A4
12674, // Tarnished Gordian Lens
12675, // Tarnished Gordian Shaft
12676, // Tarnished Gordian Crank
12677, // Tarnished Gordian Spring
12678, // Tarnished Gordian Pedal
12680, // Tarnished Gordian Bolt
13581, // Precision Gordian Bolt
13583, // Precision Gordian Lens
13585, // Precision Gordian Spring
13587, // Precision Gordian Shaft
// A5-A8
14301, // Tarnished Midan Lens
14302, // Tarnished Midan Shaft
14303, // Tarnished Midan Crank
14304, // Tarnished Midan Spring
14305, // Tarnished Midan Pedal
14307, // Tarnished Midan Bolt
// A9-A12
16545, // Alexandrian Gear
16546, // Tarnished Alexandrian Lens
16547, // Tarnished Alexandrian Shaft
16548, // Tarnished Alexandrian Crank
16549, // Tarnished Alexandrian Spring
16550, // Tarnished Alexandrian Pedal
16552, // Tarnished Alexandrian Bolt
// O1-O4
19111, // Deltascape Lens
19112, // Deltascape Shaft
19113, // Deltascape Crank
19114, // Deltascape Spring
19115, // Deltascape Pedal
19117, // Deltascape Bolt
19122, // Deltascape Crystalloid
// O5-O8
21774, // Sigmascape Lens
21775, // Sigmascape Shaft
21776, // Sigmascape Crank
21777, // Sigmascape Spring
21778, // Sigmascape Pedal
21780, // Sigmascape Bolt
// O9-O12
23963, // Alphascape Lens
23964, // Alphascape Shaft
23965, // Alphascape Crank
23966, // Alphascape Spring
23967, // Alphascape Pedal
23969, // Alphascape Bolt
// E1-E4
27393, // Helm of Early Antiquity
27394, // Armor of Early Antiquity
27395, // Gauntlets of Early Antiquity
27396, // Chausses of Early Antiquity
27397, // Greaves of Early Antiquity
27399, // Bangle of Early Antiquity
// E5-E8
29020, // Helm of Golden Antiquity
29021, // Armor of Golden Antiquity
29022, // Gauntlets of Golden Antiquity
29023, // Chausses of Golden Antiquity
29024, // Greaves of Golden Antiquity
29026, // Bangle of Golden Antiquity
// E9-E12
32133, // Helm of Lost Antiquity
32134, // Armor of Lost Antiquity
32135, // Gauntlets of Lost Antiquity
32136, // Chausses of Lost Antiquity
32137, // Greaves of Lost Antiquity
32139, // Bangle of Lost Antiquity
// P1-P4
35817, // Unsung Helm of Asphodelos
35818, // Unsung Armor of Asphodelos
35819, // Unsung Gauntlets of Asphodelos
35820, // Unsung Chausses of Asphodelos
35821, // Unsung Greaves of Asphodelos
35822, // Unsung Ring of Asphodelos
// P5-P8
38375, // Unsung Helm of Abyssos
38376, // Unsung Armor of Abyssos
38377, // Unsung Gauntlets of Abyssos
38378, // Unsung Chausses of Abyssos
38379, // Unsung Greaves of Abyssos
38380, // Unsung Ring of Abyssos
38385, // Unsung Blade of Abyssos // TODO: Remove in 7.0 as it will no longer drop
// P9-P12
40297, // Unsung Helm of Anabaseios
40298, // Unsung Armor of Anabaseios
40299, // Unsung Gauntlets of Anabaseios
40300, // Unsung Chausses of Anabaseios
40301, // Unsung Greaves of Anabaseios
40302, // Unsung Ring of Anabaseios
40317, // Unsung Blade of Anabaseios
#endregion
#region Cracked crystals/clusters
15110, // V
16783, // V
18030, // VI
18031, // VI
26819, // VII
26820, // VII
26821, // VIII
26822, // VIII
36025, // IX
36026, // IX
36027, // X
36028, // X
#endregion
#region Eureka Lootboxes
21831, // Sharlayan Cabinet
21832, // Sharlayan Wardrobe
22306, // Eurekan Potion
#endregion
#region Unapproved Firmament materials
32005, // Switch
32006, // Hemp
32007, // Iron Ore
32008, // Iron Sand
32009, // Mahogany Log
32010, // Sesame
32011, // Cotton
32012, // Ore
32013, // Rock Salt
32014, // Mythrite Sand
32015, // Spruce Log
32016, // Mistletoe
32017, // Toad
32018, // Vine
32019, // Tea Leaves
32020, // Electrum Ore
32021, // Alumen
32022, // Spring Water
32023, // Gold Sand
32024, // Ragstone
32025, // White Cedar Log
32026, // Primordial Resin
32027, // Wheat
32028, // Gossamer Cotton Boll
32029, // Tortoise
32030, // Gold Ore
32031, // Finest Rock Salt
32032, // Truespring Water
32034, // Bluespirit Ore
32033, // Mineral Sand
32035, // Log
32036, // Raspberry
32037, // Caiman
32038, // Cocoon
32039, // Barbgrass
32040, // Cloudstone
32041, // Spring Water
32042, // Ice Stalagmite
32043, // Silex
32044, // Prismstone
32045, // Galewood Branch
32046, // Umbral Dirtleaf
32047, // Umbral Flarerock
32048, // Umbral Levinsand
#endregion
#region Irregular Tomestones (after the event is over)
24909, // Philosophy
26536, // Mythology
28648, // Soldiery
30272, // Law
31339, // Esoterics
33329, // Pageantry
33330, // Lore
35834, // Scripture
36658, // Verity
38211, // Creation
39365, // Mendacity
39919, // Tenfold Pageantry
41305, // Genesis I
41306, // Genesis II
#endregion
};
private readonly Configuration _configuration;
public ListManager(Configuration configuration)
{
_configuration = configuration;
}
public void FinishInitialization()
{
_blacklistedItems = _blacklistedItems.ToImmutableHashSet();
_whitelistedItems = _whitelistedItems.ToImmutableHashSet();
}
public bool IsBlacklisted(uint itemId, bool checkConfiguration = true)
{
if (_blacklistedItems.Contains(itemId))
return true;
if (checkConfiguration && _configuration.BlacklistedItems.Contains(itemId))
return true;
return false;
}
public IReadOnlyList<uint> GetInternalBlacklist() => _blacklistedItems.ToList().AsReadOnly();
public void AddToInternalBlacklist(uint itemId) => _blacklistedItems.Add(itemId);
public bool IsWhitelisted(uint itemId) => _whitelistedItems.Contains(itemId);
public void AddToInternalWhitelist(uint itemId) => _whitelistedItems.Add(itemId);
}

View File

@ -8,36 +8,35 @@ using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib.ImGui;
namespace ARDiscard.Windows; namespace ARDiscard.Windows;
internal sealed class ConfigWindow : LImGui.LWindow internal sealed class ConfigWindow : LWindow
{ {
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly ItemCache _itemCache; private readonly ItemCache _itemCache;
private readonly IListManager _listManager;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly ICondition _condition; private readonly ICondition _condition;
private string _itemName = string.Empty; private readonly DiscardListTab _discardListTab;
private readonly ExcludedListTab _excludedListTab;
private List<(uint ItemId, string Name)> _searchResults = new(); private List<(uint ItemId, string Name)>? _allItems;
private List<(uint ItemId, string Name)> _discarding = new();
private List<(uint ItemId, string Name)>? _allItems = null;
private bool _resetKeyboardFocus = true;
public event EventHandler? DiscardNowClicked; public event EventHandler? DiscardNowClicked;
public event EventHandler? ConfigSaved; public event EventHandler? ConfigSaved;
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache, public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache,
IClientState clientState, ICondition condition) IListManager listManager, IClientState clientState, ICondition condition)
: base("Auto Discard###AutoDiscardConfig") : base("Auto Discard###AutoDiscardConfig")
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;
_itemCache = itemCache; _itemCache = itemCache;
_listManager = listManager;
_clientState = clientState; _clientState = clientState;
_condition = condition; _condition = condition;
@ -50,8 +49,11 @@ internal sealed class ConfigWindow : LImGui.LWindow
MaximumSize = new Vector2(9999, 9999), MaximumSize = new Vector2(9999, 9999),
}; };
_discarding.AddRange(_configuration.DiscardingItems _excludedListTab = new ExcludedListTab(this, itemCache, _configuration.BlacklistedItems, listManager);
.Select(x => (x, itemCache.GetItemName(x))).ToList()); _discardListTab = new DiscardListTab(this, itemCache, _configuration.DiscardingItems)
{
ExcludedTab = _excludedListTab,
};
} }
public override void Draw() public override void Draw()
@ -63,7 +65,10 @@ internal sealed class ConfigWindow : LImGui.LWindow
Save(); Save();
} }
ImGui.SameLine(ImGui.GetWindowWidth() - 115 * ImGuiHelpers.GlobalScale); ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize("Preview Discards").X -
ImGui.GetStyle().ItemSpacing.X);
ImGui.BeginDisabled(!_clientState.IsLoggedIn || ImGui.BeginDisabled(!_clientState.IsLoggedIn ||
!(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) || !(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) ||
DiscardNowClicked == null); DiscardNowClicked == null);
@ -82,6 +87,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
{ {
DrawDiscardList(); DrawDiscardList();
DrawExcludedCharacters(); DrawExcludedCharacters();
DrawExcludedItems();
DrawExperimentalSettings(); DrawExperimentalSettings();
ImGui.EndTabBar(); ImGui.EndTabBar();
@ -92,91 +98,7 @@ internal sealed class ConfigWindow : LImGui.LWindow
{ {
if (ImGui.BeginTabItem("Items to Discard")) if (ImGui.BeginTabItem("Items to Discard"))
{ {
var ws = ImGui.GetWindowSize(); _discardListTab.Draw();
if (ImGui.BeginChild("Left", new Vector2(Math.Max(10, ws.X / 2), -1), true))
{
ImGui.Text("Search");
ImGui.SetNextItemWidth(ws.X / 2 - 20);
if (_resetKeyboardFocus)
{
ImGui.SetKeyboardFocusHere();
_resetKeyboardFocus = false;
}
string previousName = _itemName;
if (ImGui.InputText("", ref _itemName, 256, ImGuiInputTextFlags.EnterReturnsTrue))
{
_resetKeyboardFocus = true;
if (_searchResults.Count > 0)
{
var itemToAdd = _searchResults.FirstOrDefault();
if (_discarding.All(x => x.ItemId != itemToAdd.ItemId))
{
_discarding.Add(itemToAdd);
}
else
{
_discarding.Remove(itemToAdd);
}
Save();
}
}
if (previousName != _itemName)
UpdateResults();
ImGui.Separator();
if (string.IsNullOrEmpty(_itemName))
{
ImGui.Text("Type item name...");
}
foreach (var (id, name) in _searchResults)
{
bool selected = _discarding.Any(x => x.Item1 == id);
if (ImGui.Selectable(name, selected))
{
if (!selected)
{
_discarding.Add((id, name));
}
else
{
_discarding.Remove((id, name));
}
Save();
}
}
}
ImGui.EndChild();
ImGui.SameLine();
if (ImGui.BeginChild("Right", new Vector2(-1, -1), true, ImGuiWindowFlags.NoSavedSettings))
{
ImGui.Text("Items that will be automatically discarded");
ImGui.Separator();
List<(uint, string)> toRemove = new();
foreach (var (id, name) in _discarding.OrderBy(x => x.Name.ToLower()))
{
if (ImGui.Selectable(name, true))
toRemove.Add((id, name));
}
if (toRemove.Count > 0)
{
foreach (var tr in toRemove)
_discarding.Remove(tr);
Save();
}
}
ImGui.EndChild();
ImGui.EndTabItem(); ImGui.EndTabItem();
} }
} }
@ -258,6 +180,19 @@ internal sealed class ConfigWindow : LImGui.LWindow
} }
} }
private void DrawExcludedItems()
{
if (ImGui.BeginTabItem("Excluded Items"))
{
ImGui.Text(
"Items configured here will never be discarded, and have priority over the 'Items to Discard' tab.");
ImGui.Text("Some items (such as Ultimate weapons) can not be un-blacklisted.");
_excludedListTab.Draw();
ImGui.EndTabItem();
}
}
private void DrawExperimentalSettings() private void DrawExperimentalSettings()
{ {
if (ImGui.BeginTabItem("Experimental Settings")) if (ImGui.BeginTabItem("Experimental Settings"))
@ -272,6 +207,14 @@ internal sealed class ConfigWindow : LImGui.LWindow
ImGui.BeginDisabled(!discardFromArmouryChest); ImGui.BeginDisabled(!discardFromArmouryChest);
ImGui.Indent(30); ImGui.Indent(30);
bool mainHandOffHand = _configuration.Armoury.CheckMainHandOffHand;
if (ImGui.Checkbox("Discard when items are found in Main Hand/Off Hand (Weapons and Tools)",
ref mainHandOffHand))
{
_configuration.Armoury.CheckMainHandOffHand = mainHandOffHand;
Save();
}
bool leftSideGear = _configuration.Armoury.CheckLeftSideGear; bool leftSideGear = _configuration.Armoury.CheckLeftSideGear;
if (ImGui.Checkbox("Discard when items are found in Head/Body/Hands/Legs/Feet", ref leftSideGear)) if (ImGui.Checkbox("Discard when items are found in Head/Body/Hands/Legs/Feet", ref leftSideGear))
{ {
@ -291,7 +234,8 @@ internal sealed class ConfigWindow : LImGui.LWindow
if (ImGui.InputInt("Ignore items >= this ilvl (Armoury Chest only)", if (ImGui.InputInt("Ignore items >= this ilvl (Armoury Chest only)",
ref maximumItemLevel)) ref maximumItemLevel))
{ {
_configuration.Armoury.MaximumGearItemLevel = Math.Max(0, Math.Min(625, maximumItemLevel)); _configuration.Armoury.MaximumGearItemLevel =
Math.Max(0, Math.Min(_itemCache.MaxDungeonItemLevel, maximumItemLevel));
Save(); Save();
} }
@ -330,6 +274,15 @@ internal sealed class ConfigWindow : LImGui.LWindow
Save(); Save();
} }
bool ignoreItemWithSignature = _configuration.IgnoreItemWithSignature;
if (ImGui.Checkbox(
"Ignore items with a 'crafted by' signature (manually crafted by yourself or someone else)",
ref ignoreItemWithSignature))
{
_configuration.IgnoreItemWithSignature = ignoreItemWithSignature;
Save();
}
ImGui.Separator(); ImGui.Separator();
bool groupPreviewByCategory = _configuration.Preview.GroupByCategory; bool groupPreviewByCategory = _configuration.Preview.GroupByCategory;
@ -350,29 +303,12 @@ internal sealed class ConfigWindow : LImGui.LWindow
} }
} }
private void UpdateResults() internal List<(uint ItemId, string Name)> EnsureAllItemsLoaded()
{
if (string.IsNullOrEmpty(_itemName))
_searchResults = new();
else
{
_searchResults = EnsureAllItemsLoaded().Where(x =>
x.Name.Contains(_itemName, StringComparison.CurrentCultureIgnoreCase)
|| (uint.TryParse(_itemName, out uint itemId) && x.ItemId == itemId))
.OrderBy(x => _itemName.EqualsIgnoreCase(x.Name) ? string.Empty : x.Name)
.ToList();
}
}
private List<(uint ItemId, string Name)> EnsureAllItemsLoaded()
{ {
if (_allItems == null) if (_allItems == null)
{ {
_allItems = _itemCache.AllItems _allItems = _itemCache.AllItems
.Where(x => InternalConfiguration.WhitelistedItems.Contains(x.ItemId) || .Where(x => x.CanBeDiscarded(_listManager, false))
x is { IsUnique: false, IsUntradable: false, IsIndisposable: false })
.Where(x => x.UiCategory != UiCategories.Currency && x.UiCategory != UiCategories.Crystals &&
x.UiCategory != UiCategories.Unobtainable)
.Select(x => (x.ItemId, x.Name.ToString())) .Select(x => (x.ItemId, x.Name.ToString()))
.ToList(); .ToList();
} }
@ -380,37 +316,18 @@ internal sealed class ConfigWindow : LImGui.LWindow
return _allItems; return _allItems;
} }
private void Save() internal void Save()
{ {
_configuration.DiscardingItems = _discarding.Select(x => x.ItemId).ToList(); _configuration.DiscardingItems = _discardListTab.ToSavedItems().ToList();
_configuration.BlacklistedItems = _excludedListTab.ToSavedItems().ToList();
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
ConfigSaved?.Invoke(this, EventArgs.Empty); ConfigSaved?.Invoke(this, EventArgs.Empty);
} }
internal bool AddToDiscardList(uint itemId) internal bool AddToDiscardList(uint itemId) => _discardListTab.AddToDiscardList(itemId);
{
var item = EnsureAllItemsLoaded().SingleOrDefault(x => x.ItemId == itemId);
if (item.ItemId != 0)
{
_discarding.Add(item);
Save();
return true;
}
return false; internal bool RemoveFromDiscardList(uint itemId) => _discardListTab.RemoveFromDiscardList(itemId);
}
internal bool RemoveFromDiscardList(uint itemId)
{
if (_discarding.RemoveAll(x => x.ItemId == itemId) > 0)
{
Save();
return true;
}
return false;
}
public bool CanItemBeConfigured(uint itemId) public bool CanItemBeConfigured(uint itemId)
{ {

View File

@ -0,0 +1,55 @@
using System.Collections.Generic;
using System.Linq;
using ARDiscard.GameData;
namespace ARDiscard.Windows
{
internal sealed class DiscardListTab : ItemListTab
{
public DiscardListTab(ConfigWindow parent, ItemCache itemCache, List<uint> initialItems)
: base(parent, itemCache, initialItems)
{
}
protected override string RightSideLabel => "Items that will be automatically discarded";
internal required ExcludedListTab ExcludedTab { private get; init; }
public IEnumerable<uint> ToSavedItems()
{
SelectedItems.RemoveAll(x => ExcludedTab.IsBlacklistedInConfiguration(x.ItemId));
return SelectedItems.Select(x => x.ItemId);
}
protected override (string, bool) AsLeftSideDisplay(uint itemId, string name)
{
if (ExcludedTab.IsBlacklistedInConfiguration(itemId))
return ($"{name} (Blacklisted/Excluded Item)", false);
return base.AsLeftSideDisplay(itemId, name);
}
internal bool AddToDiscardList(uint itemId)
{
var item = EnsureAllItemsLoaded().SingleOrDefault(x => x.ItemId == itemId);
if (item.ItemId != 0)
{
SelectedItems.Add(item);
Save();
return true;
}
return false;
}
internal bool RemoveFromDiscardList(uint itemId)
{
if (SelectedItems.RemoveAll(x => x.ItemId == itemId) > 0)
{
Save();
return true;
}
return false;
}
}
}

View File

@ -1,18 +1,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization;
using System.Linq; using System.Linq;
using ARDiscard.GameData; using ARDiscard.GameData;
using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Internal; using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility; using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Common.Math; using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET; using ImGuiNET;
using LLib; using LLib;
using LLib.ImGui;
namespace ARDiscard.Windows; namespace ARDiscard.Windows;
internal sealed class DiscardWindow : LImGui.LWindow internal sealed class DiscardWindow : LWindow
{ {
private readonly InventoryUtils _inventoryUtils; private readonly InventoryUtils _inventoryUtils;
private readonly ItemCache _itemCache; private readonly ItemCache _itemCache;
@ -47,7 +51,7 @@ internal sealed class DiscardWindow : LImGui.LWindow
}; };
} }
public bool Locked { get; set; } = false; public bool Locked { get; set; }
public override void Draw() public override void Draw()
{ {
@ -94,11 +98,15 @@ internal sealed class DiscardWindow : LImGui.LWindow
if (ImGui.Button("Open Configuration")) if (ImGui.Button("Open Configuration"))
OpenConfigurationClicked!.Invoke(this, EventArgs.Empty); OpenConfigurationClicked!.Invoke(this, EventArgs.Empty);
ImGui.EndDisabled(); ImGui.EndDisabled();
ImGui.SameLine(ImGui.GetWindowWidth() - 160 * ImGuiHelpers.GlobalScale);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize("Discard all selected items").X -
ImGui.GetStyle().ItemSpacing.X);
ImGui.BeginDisabled(Locked || ImGui.BeginDisabled(Locked ||
!_clientState.IsLoggedIn || !_clientState.IsLoggedIn ||
!(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) || !(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) ||
_displayedItems.Count(x => x.Selected) == 0 || !_displayedItems.Any(x => x.Selected) ||
DiscardAllClicked == null); DiscardAllClicked == null);
if (ImGui.Button("Discard all selected items")) if (ImGui.Button("Discard all selected items"))
{ {
@ -115,12 +123,14 @@ internal sealed class DiscardWindow : LImGui.LWindow
{ {
if (_configuration.Preview.ShowIcons) if (_configuration.Preview.ShowIcons)
{ {
IDalamudTextureWrap? icon = _iconCache.GetIcon(displayedItem.IconId); ISharedImmediateTexture icon = _iconCache.GetIcon(displayedItem.IconId);
if (icon != null) if (icon.TryGetWrap(out IDalamudTextureWrap? wrap, out _))
{ {
ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); ImGui.Image(wrap.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine(); ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
wrap.Dispose();
} }
} }
@ -148,8 +158,8 @@ internal sealed class DiscardWindow : LImGui.LWindow
_displayedItems = _inventoryUtils.GetAllItemsToDiscard() _displayedItems = _inventoryUtils.GetAllItemsToDiscard()
.GroupBy(x => new .GroupBy(x => new
{ {
ItemId = x.InventoryItem->ItemID, ItemId = x.InventoryItem->ItemId,
ItemInfo = _itemCache.GetItem(x.InventoryItem->ItemID), ItemInfo = _itemCache.GetItem(x.InventoryItem->ItemId),
}) })
.Where(x => x.Key.ItemInfo != null) .Where(x => x.Key.ItemInfo != null)
.Select(x => new SelectableItem .Select(x => new SelectableItem
@ -162,7 +172,7 @@ internal sealed class DiscardWindow : LImGui.LWindow
UiCategoryName = x.Key.ItemInfo!.UiCategoryName, UiCategoryName = x.Key.ItemInfo!.UiCategoryName,
Selected = !notSelected.Contains(x.Key.ItemId), Selected = !notSelected.Contains(x.Key.ItemId),
}) })
.OrderBy(x => x.Name.ToLower()) .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToList(); .ToList();
} }

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.Linq;
using ARDiscard.GameData;
namespace ARDiscard.Windows;
internal sealed class ExcludedListTab : ItemListTab
{
private readonly IListManager _listManager;
public ExcludedListTab(ConfigWindow parent, ItemCache itemCache, List<uint> initialItems, IListManager listManager)
: base(parent, itemCache, initialItems)
{
_listManager = listManager;
SelectedItems.AddRange(
listManager.GetInternalBlacklist()
.Select(x => (x, itemCache.GetItemName(x)))
.Where(x => x.Item1 >= 100 && !string.IsNullOrEmpty(x.Item2)));
}
protected override string RightSideLabel => "Items that will never be discarded";
public IEnumerable<uint> ToSavedItems()
{
return SelectedItems
.Select(x => x.ItemId)
.Where(x => !_listManager.IsBlacklisted(x, false));
}
public bool IsBlacklistedInConfiguration(uint itemId)
=> !_listManager.IsBlacklisted(itemId, false) && SelectedItems.Any(x => x.ItemId == itemId);
protected override (string Name, bool Enabled) AsLeftSideDisplay(uint itemId, string name)
=> (name, !_listManager.IsBlacklisted(itemId, false));
}

View File

@ -0,0 +1,161 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ARDiscard.GameData;
using ImGuiNET;
namespace ARDiscard.Windows;
internal abstract class ItemListTab
{
private const int ResultLimit = 200;
private readonly ConfigWindow _parent;
private List<(uint ItemId, string Name)> _searchResults = new();
private string _itemName = string.Empty;
private bool _resetKeyboardFocus = true;
protected ItemListTab(ConfigWindow parent, ItemCache itemCache, List<uint> initialItems)
{
_parent = parent;
SelectedItems.AddRange(initialItems.Select(x => (x, itemCache.GetItemName(x))).ToList());
}
protected abstract string RightSideLabel { get; }
protected List<(uint ItemId, string Name)> SelectedItems { get; } = new();
public void Draw()
{
var ws = ImGui.GetWindowSize();
if (ImGui.BeginChild("Left", new Vector2(Math.Max(10, ws.X / 2), -1), true))
{
if (!string.IsNullOrEmpty(_itemName))
{
if (_searchResults.Count > ResultLimit)
ImGui.Text($"Search ({ResultLimit:N0} out of {_searchResults.Count:N0} matches)");
else
ImGui.Text($"Search ({_searchResults.Count:N0} matches)");
}
else
ImGui.Text("Search");
ImGui.SetNextItemWidth(ws.X / 2 - 20);
if (_resetKeyboardFocus)
{
ImGui.SetKeyboardFocusHere();
_resetKeyboardFocus = false;
}
string previousName = _itemName;
if (ImGui.InputText("", ref _itemName, 256, ImGuiInputTextFlags.EnterReturnsTrue))
{
_resetKeyboardFocus = true;
if (_searchResults.Count > 0)
{
var itemToAdd = _searchResults.FirstOrDefault();
if (SelectedItems.All(x => x.ItemId != itemToAdd.ItemId))
{
SelectedItems.Add(itemToAdd);
}
else
{
SelectedItems.Remove(itemToAdd);
}
Save();
}
}
if (previousName != _itemName)
UpdateResults();
ImGui.Separator();
if (string.IsNullOrEmpty(_itemName))
{
ImGui.Text("Type item name...");
}
foreach (var (id, name) in _searchResults.Take(ResultLimit))
{
bool selected = SelectedItems.Any(x => x.Item1 == id);
var display = AsLeftSideDisplay(id, name);
if (!display.Enabled)
ImGui.BeginDisabled();
if (ImGui.Selectable($"{display.Name}##Item{id}", selected))
{
if (!selected)
{
SelectedItems.Add((id, name));
}
else
{
SelectedItems.Remove((id, name));
}
Save();
}
if (!display.Enabled)
ImGui.EndDisabled();
}
}
ImGui.EndChild();
ImGui.SameLine();
if (ImGui.BeginChild("Right", new Vector2(-1, -1), true, ImGuiWindowFlags.NoSavedSettings))
{
ImGui.Text(RightSideLabel);
ImGui.Separator();
List<(uint, string)> toRemove = new();
foreach (var (id, name) in SelectedItems.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase))
{
var display = AsLeftSideDisplay(id, name);
if (!display.Enabled)
ImGui.BeginDisabled();
if (ImGui.Selectable($"{display.Name}##Item{id}", true))
toRemove.Add((id, name));
if (!display.Enabled)
ImGui.EndDisabled();
}
if (toRemove.Count > 0)
{
foreach (var tr in toRemove)
SelectedItems.Remove(tr);
Save();
}
}
ImGui.EndChild();
}
protected virtual (string Name, bool Enabled) AsLeftSideDisplay(uint itemId, string name) => (name, true);
protected void Save() => _parent.Save();
private void UpdateResults()
{
if (string.IsNullOrEmpty(_itemName))
_searchResults = new();
else
{
_searchResults = EnsureAllItemsLoaded().Where(x =>
x.Name.Contains(_itemName, StringComparison.CurrentCultureIgnoreCase)
|| (uint.TryParse(_itemName, out uint itemId) && x.ItemId == itemId))
.OrderBy(x => _itemName.Equals(x.Name, StringComparison.OrdinalIgnoreCase) ? string.Empty : x.Name)
.ToList();
}
}
protected List<(uint ItemId, string Name)> EnsureAllItemsLoaded() => _parent.EnsureAllItemsLoaded();
}

View File

@ -1,30 +1,95 @@
{ {
"version": 1, "version": 1,
"dependencies": { "dependencies": {
"net7.0-windows7.0": { "net8.0-windows7.0": {
"Dalamud.ContextMenu": {
"type": "Direct",
"requested": "[1.3.1, )",
"resolved": "1.3.1",
"contentHash": "ptAxut5PiLnzZ4G/KQdHJVcyklC/BF3otHJ7zYVUPiKBjsOCoF0n/6h2jK7e+8ev2Y1yAY3Wtx2GuXLFQgt9Uw=="
},
"DalamudPackager": { "DalamudPackager": {
"type": "Direct", "type": "Direct",
"requested": "[2.1.12, )", "requested": "[2.1.13, )",
"resolved": "2.1.12", "resolved": "2.1.13",
"contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" "contentHash": "rMN1omGe8536f4xLMvx9NwfvpAc9YFFfeXJ1t4P4PE6Gu8WCIoFliR1sh07hM+bfODmesk/dvMbji7vNI+B/pQ=="
},
"DotNet.ReproducibleBuilds": {
"type": "Direct",
"requested": "[1.1.1, )",
"resolved": "1.1.1",
"contentHash": "+H2t/t34h6mhEoUvHi8yGXyuZ2GjSovcGYehJrS2MDm2XgmPfZL2Sdxg+uL2lKgZ4M6tTwKHIlxOob2bgh0NRQ==",
"dependencies": {
"Microsoft.SourceLink.AzureRepos.Git": "1.1.1",
"Microsoft.SourceLink.Bitbucket.Git": "1.1.1",
"Microsoft.SourceLink.GitHub": "1.1.1",
"Microsoft.SourceLink.GitLab": "1.1.1"
}
},
"Microsoft.SourceLink.Gitea": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "KOBodmDnlWGIqZt2hT47Q69TIoGhIApDVLCyyj9TT5ct8ju16AbHYcB4XeknoHX562wO1pMS/1DfBIZK+V+sxg==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.SourceLink.AzureRepos.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "qB5urvw9LO2bG3eVAkuL+2ughxz2rR7aYgm2iyrB8Rlk9cp2ndvGRCvehk3rNIhRuNtQaeKwctOl1KvWiklv5w==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Bitbucket.Git": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "cDzxXwlyWpLWaH0em4Idj0H3AmVo3L/6xRXKssYemx+7W52iNskj/SQ4FOmfCb8YQt39otTDNMveCZzYtMoucQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
},
"Microsoft.SourceLink.GitLab": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "tvsg47DDLqqedlPeYVE2lmiTpND8F0hkrealQ5hYltSmvruy/Gr5nHAKSsjyw5L3NeM/HLMI5ORv7on/M4qyZw==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "1.1.1",
"Microsoft.SourceLink.Common": "1.1.1"
}
}, },
"autoretainerapi": { "autoretainerapi": {
"type": "Project", "type": "Project",
"dependencies": { "dependencies": {
"ECommons": "[2.1.0, )" "ECommons": "[2.2.0.2, )"
} }
}, },
"ecommons": { "ecommons": {
"type": "Project" "type": "Project"
}, },
"llib": { "llib": {
"type": "Project" "type": "Project",
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
} }
} }
} }

@ -1 +1 @@
Subproject commit 7cb54772e3a4a60ad02520e898d1ed0e82b2a751 Subproject commit a63c8e7154e272374ffa03d5c801736d4229e38a

@ -1 +1 @@
Subproject commit f1c688a0599b41d70230021328a575da7351cf91 Subproject commit 38080f2a3733aa19b6928f4d2984fac7b9a7fab7

2
LLib

@ -1 +1 @@
Subproject commit 865a6080319f8ccbcd5fd5b0004404822b6e60d4 Subproject commit 7027d291efbbff6a55944dd521d3907210ddecbe

7
global.json Normal file
View File

@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.0",
"rollForward": "latestMajor",
"allowPrerelease": true
}
}