Compare commits

..

No commits in common. "master" and "v2.0" have entirely different histories.
master ... v2.0

29 changed files with 516 additions and 3083 deletions

9
.gitmodules vendored
View File

@ -1,9 +0,0 @@
[submodule "LLib"]
path = LLib
url = https://git.carvel.li/liza/LLib.git
[submodule "AutoRetainerAPI"]
path = AutoRetainerAPI
url = https://github.com/PunishXIV/AutoRetainerAPI.git
[submodule "ECommons"]
path = ECommons
url = https://github.com/NightmareXIV/ECommons.git

View File

@ -2,12 +2,6 @@
Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARDiscard", "ARDiscard\ARDiscard.csproj", "{A9B4C542-1C83-4F46-81E7-5BAFCA109767}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{2E159617-A667-4EC8-B3A8-8B087315C181}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoRetainerAPI", "AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj", "{265C467A-3687-42AE-A178-CA7186852B38}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECommons", "ECommons\ECommons\ECommons.csproj", "{F4483C28-62CF-4EB2-A51E-FF0CF4B1E727}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -18,17 +12,5 @@ Global
{A9B4C542-1C83-4F46-81E7-5BAFCA109767}.Debug|Any CPU.Build.0 = Debug|Any CPU
{A9B4C542-1C83-4F46-81E7-5BAFCA109767}.Release|Any CPU.ActiveCfg = Release|Any CPU
{A9B4C542-1C83-4F46-81E7-5BAFCA109767}.Release|Any CPU.Build.0 = Release|Any CPU
{2E159617-A667-4EC8-B3A8-8B087315C181}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2E159617-A667-4EC8-B3A8-8B087315C181}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2E159617-A667-4EC8-B3A8-8B087315C181}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2E159617-A667-4EC8-B3A8-8B087315C181}.Release|Any CPU.Build.0 = Release|Any CPU
{265C467A-3687-42AE-A178-CA7186852B38}.Debug|Any CPU.ActiveCfg = Debug|x64
{265C467A-3687-42AE-A178-CA7186852B38}.Debug|Any CPU.Build.0 = Debug|x64
{265C467A-3687-42AE-A178-CA7186852B38}.Release|Any CPU.ActiveCfg = Release|x64
{265C467A-3687-42AE-A178-CA7186852B38}.Release|Any CPU.Build.0 = Release|x64
{F4483C28-62CF-4EB2-A51E-FF0CF4B1E727}.Debug|Any CPU.ActiveCfg = Debug|x64
{F4483C28-62CF-4EB2-A51E-FF0CF4B1E727}.Debug|Any CPU.Build.0 = Debug|x64
{F4483C28-62CF-4EB2-A51E-FF0CF4B1E727}.Release|Any CPU.ActiveCfg = Release|x64
{F4483C28-62CF-4EB2-A51E-FF0CF4B1E727}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,14 +1,7 @@
{
"Name": "Discard Helper",
"Name": "Discard after AutoRetainer",
"Author": "Liza Carvelli",
"Punchline": "Discard items automatically via a command or as a post-venture task for AutoRetainer",
"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",
"IconUrl": "https://plugins.carvel.li/icons/ARDiscard.png",
"Tags": [
"autoretainer",
"automation",
"discard",
"yeet"
]
"Punchline": "",
"Description": "",
"RepoUrl": "https://git.carvel.li/liza/ARDiscard"
}

View File

@ -1,130 +1,68 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ARDiscard.External;
using ARDiscard.GameData;
using ARDiscard.Windows;
using AutoRetainerAPI;
using ClickLib.Clicks;
using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Game.Command;
using Dalamud.Game.Gui;
using Dalamud.Interface.Windowing;
using Dalamud.Logging;
using Dalamud.Memory;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using ECommons.Automation.NeoTaskManager;
using ECommons.Automation;
using ECommons.DalamudServices;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI;
using FFXIVClientStructs.FFXIV.Component.GUI;
using LLib;
namespace ARDiscard;
[SuppressMessage("ReSharper", "UnusedType.Global")]
public sealed class AutoDiscardPlogon : IDalamudPlugin
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 IDalamudPluginInterface _pluginInterface;
private readonly IChatGui _chatGui;
private readonly IClientState _clientState;
private readonly IPluginLog _pluginLog;
private readonly IGameGui _gameGui;
private readonly ICommandManager _commandManager;
private readonly DalamudPluginInterface _pluginInterface;
private readonly ChatGui _chatGui;
private readonly ClientState _clientState;
private readonly CommandManager _commandManager;
private readonly InventoryUtils _inventoryUtils;
private readonly IconCache _iconCache;
private readonly GameStrings _gameStrings;
private readonly AutoRetainerApi _autoRetainerApi;
[SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "Obsolete in ECommons")]
private readonly TaskManager _taskManager;
private readonly ContextMenuIntegration _contextMenuIntegration;
private readonly AutoDiscardIpc _autoDiscardIpc;
private DateTime _cancelDiscardAfter = DateTime.MaxValue;
[SuppressMessage("Maintainability", "CA1506")]
public AutoDiscardPlogon(IDalamudPluginInterface pluginInterface, ICommandManager commandManager, IChatGui chatGui,
IDataManager dataManager, IClientState clientState, ICondition condition, IPluginLog pluginLog,
IGameGui gameGui, ITextureProvider textureProvider, IContextMenu contextMenu)
public AutoDiscardPlogon(DalamudPluginInterface pluginInterface, CommandManager commandManager, ChatGui chatGui,
DataManager dataManager, ClientState clientState)
{
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
MigrateConfiguration(_configuration);
_chatGui = chatGui;
_clientState = clientState;
_pluginLog = pluginLog;
_gameGui = gameGui;
_commandManager = commandManager;
_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",
});
ListManager listManager = new ListManager(_configuration);
ItemCache itemCache = new ItemCache(dataManager, listManager);
_inventoryUtils = new InventoryUtils(_configuration, itemCache, listManager, _pluginLog);
listManager.FinishInitialization();
_iconCache = new IconCache(textureProvider);
_gameStrings = new GameStrings(dataManager, pluginLog);
_commandManager.AddHandler("/discardconfig", new CommandInfo(OpenConfig));
_commandManager.AddHandler("/discardall", new CommandInfo(ProcessCommand));
_inventoryUtils = new(_configuration);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += OpenDiscardUi;
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
_discardWindow = new(_inventoryUtils, itemCache, _iconCache, clientState, condition, _configuration);
_windowSystem.AddWindow(_discardWindow);
_configWindow = new(_pluginInterface, _configuration, itemCache, listManager, clientState, condition);
_configWindow = new(_pluginInterface, _configuration, dataManager, clientState);
_windowSystem.AddWindow(_configWindow);
_configWindow.DiscardNowClicked += (_, _) => OpenDiscardWindow(string.Empty, string.Empty);
_configWindow.ConfigSaved += (_, _) => _discardWindow.RefreshInventory(true);
_discardWindow.OpenConfigurationClicked += (_, _) => OpenConfigUi();
_discardWindow.DiscardAllClicked += (_, filter) =>
{
_taskManager?.Abort();
_taskManager?.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, filter));
};
ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new();
_taskManager = new();
_contextMenuIntegration = new(_chatGui, itemCache, _configuration, listManager, _configWindow, _gameGui, contextMenu);
_autoDiscardIpc = new(_pluginInterface, _configuration);
_clientState.Login += _discardWindow.Login;
_clientState.Logout += _discardWindow.Logout;
_autoRetainerApi.OnRetainerPostprocessStep += CheckRetainerPostProcess;
_autoRetainerApi.OnRetainerReadyToPostprocess += DoRetainerPostProcess;
_autoRetainerApi.OnCharacterPostprocessStep += CheckCharacterPostProcess;
_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);
}
}
public string Name => "Discard after AutoRetainer";
private void CheckRetainerPostProcess(string retainerName) =>
CheckPostProcessInternal(PostProcessType.Retainer, retainerName, _configuration.RunAfterVenture);
@ -136,19 +74,19 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
{
if (!enabled)
{
_pluginLog.Information($"Not running post-venture tasks for {name}, disabled globally");
PluginLog.Information($"Not running post-venture tasks for {name}, disabled globally");
}
else if (_configuration.ExcludedCharacters.Any(x => x.LocalContentId == _clientState.LocalContentId))
{
_pluginLog.Information($"Not running post-venture tasks for {name}, disabled for current character");
PluginLog.Information($"Not running post-venture tasks for {name}, disabled for current character");
}
else if (_inventoryUtils.GetNextItemToDiscard(ItemFilter.None) == null)
else if (_inventoryUtils.GetNextItemToDiscard() == null)
{
_pluginLog.Information($"Not running post-venture tasks for {name}, no items to discard");
PluginLog.Information($"Not running post-venture tasks for {name}, no items to discard");
}
else
{
_pluginLog.Information($"Requesting post-processing for {name}");
PluginLog.Information($"Requesting post-processing for {name}");
if (type == PostProcessType.Retainer)
_autoRetainerApi.RequestRetainerPostprocess();
else if (type == PostProcessType.Character)
@ -158,185 +96,166 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
private void DoRetainerPostProcess(string retainerName)
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer, ItemFilter.None));
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer));
}
private void DoCharacterPostProcess()
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character, ItemFilter.None));
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character));
}
private void OpenConfig(string command, string arguments) => OpenConfigUi();
private void OpenConfigUi()
{
_configWindow.IsOpen = !_configWindow.IsOpen;
_configWindow.IsOpen = true;
}
private void DiscardAll(string command, string arguments)
private void ProcessCommand(string command, string arguments)
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, ItemFilter.None));
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.FromCommand));
}
private void OpenDiscardWindow(string command, string arguments) => OpenDiscardUi();
private void OpenDiscardUi()
private unsafe void DiscardNextItem(PostProcessType type)
{
_discardWindow.IsOpen = !_discardWindow.IsOpen;
}
private unsafe void DiscardNextItem(PostProcessType type, ItemFilter? itemFilter)
{
_pluginLog.Information($"DiscardNextItem (type = {type})");
_discardWindow.Locked = true;
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
PluginLog.Information($"DiscardNextItem (type = {type})");
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard();
if (nextItem == null)
{
_pluginLog.Information("No item to discard found");
FinishDiscarding(type);
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.");
}
else
{
var (inventoryType, slot) = (nextItem->Container, nextItem->Slot);
_pluginLog.Information(
$"Discarding itemId {nextItem->ItemId} in slot {nextItem->Slot} of container {nextItem->Container}.");
PluginLog.Information(
$"Discarding itemId {nextItem->ItemID} in slot {nextItem->Slot} of container {nextItem->Container}.");
_inventoryUtils.Discard(nextItem);
_cancelDiscardAfter = DateTime.Now.AddSeconds(15);
_taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot));
_taskManager.DelayNext(20);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, inventoryType, slot));
}
}
private unsafe void ConfirmDiscardItem(PostProcessType type, ItemFilter? itemFilter, InventoryType inventoryType,
short slot)
private unsafe void ConfirmDiscardItem(PostProcessType type, InventoryType inventoryType, short slot)
{
var addon = GetDiscardAddon();
if (addon != null)
{
_pluginLog.Verbose("Addon is visible, clicking 'yes'");
PluginLog.Information("Addon is visible, clicking 'yes'");
((AddonSelectYesno*)addon)->YesButton->AtkComponentBase.SetEnabledState(true);
addon->FireCallbackInt(0);
ClickSelectYesNo.Using((nint)addon).Yes();
_taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot));
_taskManager.DelayNext(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, inventoryType, slot));
}
else
{
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard();
if (nextItem == null)
{
_pluginLog.Information("Addon is not visible, but next item is also no longer set");
FinishDiscarding(type);
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.");
}
else if (nextItem->Container == inventoryType && nextItem->Slot == slot)
{
_pluginLog.Information(
PluginLog.Information(
$"Addon is not (yet) visible, still trying to discard item in slot {slot} in inventory {inventoryType}");
_taskManager.EnqueueDelay(100);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot));
_taskManager.DelayNext(100);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, inventoryType, slot));
}
else
{
_pluginLog.Information(
PluginLog.Information(
$"Addon is not (yet) visible, but slot or inventory type changed, retrying from start");
_taskManager.EnqueueDelay(100);
_taskManager.Enqueue(() => DiscardNextItem(type, itemFilter));
_taskManager.DelayNext(100);
_taskManager.Enqueue(() => DiscardNextItem(type));
}
}
}
private unsafe void ContinueAfterDiscard(PostProcessType type, ItemFilter? itemFilter, InventoryType inventoryType,
short slot)
private unsafe void ContinueAfterDiscard(PostProcessType type, InventoryType inventoryType, short slot)
{
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard();
if (nextItem == null)
{
_pluginLog.Information($"Continuing after discard: no next item (type = {type})");
FinishDiscarding(type);
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.");
}
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");
FinishDiscarding(type, "Discarding probably failed due to an error.");
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.");
}
else
{
_pluginLog.Verbose(
$"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}");
_taskManager.EnqueueDelay(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot));
PluginLog.Information($"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}");
_taskManager.DelayNext(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, inventoryType, slot));
}
}
else
{
_pluginLog.Information("ContinueAfterDiscard: Discovered different item to discard");
_taskManager.Enqueue(() => DiscardNextItem(type, itemFilter));
PluginLog.Information($"ContinueAfterDiscard: Discovered different item to discard");
_taskManager.EnqueueImmediate(() => DiscardNextItem(type));
}
}
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;
_autoDiscardIpc.Dispose();
_contextMenuIntegration.Dispose();
_autoRetainerApi.Dispose();
ECommonsMain.Dispose();
_iconCache.Dispose();
_inventoryUtils.Dispose();
_pluginInterface.UiBuilder.OpenConfigUi -= OpenConfigUi;
_pluginInterface.UiBuilder.OpenMainUi -= OpenDiscardUi;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_commandManager.RemoveHandler("/discard");
_commandManager.RemoveHandler("/discardall");
_commandManager.RemoveHandler("/discardconfig");
}
private unsafe AtkUnitBase* GetDiscardAddon()
private static unsafe AtkUnitBase* GetDiscardAddon()
{
for (int i = 1; i < 100; i++)
{
try
{
var addon = (AtkUnitBase*)_gameGui.GetAddonByName("SelectYesno", i);
var addon = (AtkUnitBase*)Svc.GameGui.GetAddonByName("SelectYesno", i);
if (addon == null) return null;
if (addon->IsVisible && addon->UldManager.LoadedState == AtkLoadState.Loaded)
{
var textNode = addon->UldManager.NodeList[15]->GetAsAtkTextNode();
var text = MemoryHelper.ReadSeString(&textNode->NodeText).ExtractText();
_pluginLog.Information($"YesNo prompt: {text}");
if (_gameStrings.DiscardItem.IsMatch(text) || _gameStrings.DiscardCollectable.IsMatch(text))
PluginLog.Information($"YesNo prompt: {text}");
if (text.StartsWith("Discard"))
{
return addon;
}
@ -355,6 +274,6 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
{
Retainer,
Character,
ManuallyStarted,
FromCommand,
}
}

276
ARDiscard/ConfigWindow.cs Normal file
View File

@ -0,0 +1,276 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using Dalamud.Data;
using Dalamud.Game.ClientState;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using ECommons;
using ImGuiNET;
using Lumina.Excel.GeneratedSheets;
namespace ARDiscard;
public class ConfigWindow : Window
{
private readonly DalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly DataManager _dataManager;
private readonly ClientState _clientState;
private string _itemName = string.Empty;
private List<(uint ItemId, string Name)> _searchResults = new();
private List<(uint ItemId, string Name)> _discarding = new();
private List<(uint ItemId, string Name)>? _allItems = null;
private bool _resetKeyboardFocus = true;
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, DataManager dataManager,
ClientState clientState)
: base("Auto Discard###AutoDiscardConfig")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
_dataManager = dataManager;
_clientState = clientState;
Size = new Vector2(600, 400);
SizeCondition = ImGuiCond.FirstUseEver;
SizeConstraints = new WindowSizeConstraints
{
MinimumSize = new Vector2(600, 400),
MaximumSize = new Vector2(9999, 9999),
};
_discarding.AddRange(_configuration.DiscardingItems
.Select(x => (x, dataManager.GetExcelSheet<Item>()?.GetRow(x)?.Name?.ToString() ?? x.ToString())).ToList());
}
public override void Draw()
{
bool runAfterVenture = _configuration.RunAfterVenture;
if (ImGui.Checkbox("[Global] Run automatically after AutoRetainer's venture", ref runAfterVenture))
{
_configuration.RunAfterVenture = runAfterVenture;
Save();
}
bool runBeforeLogout = _configuration.RunBeforeLogout;
if (ImGui.Checkbox("[Global] Run before logging out in Multi-Mode", ref runBeforeLogout))
{
_configuration.RunBeforeLogout = runBeforeLogout;
Save();
}
if (ImGui.BeginTabBar("AutoDiscardTabs"))
{
DrawDiscardList();
DrawExcludedCharacters();
ImGui.EndTabBar();
}
}
private void DrawDiscardList()
{
if (ImGui.BeginTabItem("Items to Discard"))
{
var ws = ImGui.GetWindowSize();
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();
}
}
private void DrawExcludedCharacters()
{
if (ImGui.BeginTabItem("Excluded Characters"))
{
if (_clientState.IsLoggedIn && _clientState.LocalContentId > 0)
{
string worldName = _clientState.LocalPlayer?.HomeWorld.GameData?.Name ?? "??";
ImGui.TextWrapped(
$"Current Character: {_clientState.LocalPlayer?.Name} @ {worldName} ({_clientState.LocalContentId:X})");
ImGui.Indent(30);
if (_configuration.ExcludedCharacters.Any(x => x.LocalContentId == _clientState.LocalContentId))
{
ImGui.TextColored(ImGuiColors.DalamudRed, "This character is currently excluded.");
if (ImGui.Button("Remove exclusion"))
{
_configuration.ExcludedCharacters.RemoveAll(
c => c.LocalContentId == _clientState.LocalContentId);
Save();
}
}
else
{
if (_configuration.RunAfterVenture || _configuration.RunBeforeLogout)
{
ImGui.TextColored(ImGuiColors.HealerGreen,
"This character is currently included (and will be post-processed in autoretainer).");
}
else
{
ImGui.TextColored(ImGuiColors.DalamudYellow,
"This character is currently included (but running post-processing is disabled globally)");
}
if (ImGui.Button("Exclude current character"))
{
_configuration.ExcludedCharacters.Add(new Configuration.CharacterInfo
{
LocalContentId = _clientState.LocalContentId,
CachedPlayerName = _clientState.LocalPlayer?.Name.ToString() ?? "??",
CachedWorldName = worldName,
});
Save();
}
}
ImGui.Unindent(30);
}
else
{
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not logged in.");
}
ImGui.Separator();
ImGui.TextWrapped(
"Characters that won't run auto-cleanup after ventures (/discardall works for excluded characters)");
ImGui.Spacing();
ImGui.Indent(30);
if (_configuration.ExcludedCharacters.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "No excluded characters.");
}
else
{
foreach (var characterInfo in _configuration.ExcludedCharacters)
{
ImGui.Text(
$"{characterInfo.CachedPlayerName} @ {characterInfo.CachedWorldName} ({characterInfo.LocalContentId:X})");
}
}
ImGui.Unindent(30);
ImGui.EndTabItem();
}
}
private void UpdateResults()
{
if (string.IsNullOrEmpty(_itemName))
_searchResults = new();
else
{
if (_allItems == null)
{
_allItems = _dataManager.GetExcelSheet<Item>()!
.Where(x => x.RowId != 0)
.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()))
.ToList();
}
_searchResults = _allItems.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 void Save()
{
_configuration.DiscardingItems = _discarding.Select(x => x.ItemId).ToList();
_pluginInterface.SavePluginConfig(_configuration);
}
}

View File

@ -3,46 +3,18 @@ using Dalamud.Configuration;
namespace ARDiscard;
internal sealed class Configuration : IPluginConfiguration
public class Configuration : IPluginConfiguration
{
public int Version { get; set; } = 2;
public int Version { get; set; } = 1;
public bool RunAfterVenture { get; set; }
public bool RunBeforeLogout { get; set; }
public List<uint> DiscardingItems { get; set; } = new();
public List<uint> BlacklistedItems { get; set; } = new();
public List<CharacterInfo> ExcludedCharacters { get; set; } = new();
public ArmouryConfiguration Armoury { get; set; } = new();
public ContextMenuConfiguration ContextMenu { get; set; } = new();
public PreviewConfiguration Preview { get; set; } = new();
public uint IgnoreItemCountWhenAbove { get; set; } = 50;
public bool IgnoreItemWithSignature { get; set; }
public sealed class CharacterInfo
public 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; }
public bool CheckMainHandOffHand { get; set; }
public bool CheckLeftSideGear { get; set; }
public bool CheckRightSideGear { get; set; }
public int MaximumGearItemLevel { get; set; } = 45;
}
public sealed class ContextMenuConfiguration
{
public bool Enabled { get; set; } = true;
public bool OnlyWhenConfigIsOpen { get; set; }
}
public sealed class PreviewConfiguration
{
public bool GroupByCategory { get; set; } = true;
public bool ShowIcons { get; set; } = true;
public string CachedPlayerName { get; set; }
public string CachedWorldName { get; set; }
}
}

View File

@ -1,164 +0,0 @@
using System;
using ARDiscard.GameData;
using ARDiscard.Windows;
using Dalamud.Game.Gui.ContextMenu;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Plugin.Services;
namespace ARDiscard;
internal sealed class ContextMenuIntegration : IDisposable
{
private readonly IChatGui _chatGui;
private readonly ItemCache _itemCache;
private readonly Configuration _configuration;
private readonly IListManager _listManager;
private readonly ConfigWindow _configWindow;
private readonly IGameGui _gameGui;
private readonly IContextMenu _contextMenu;
private readonly MenuItem _addInventoryItem;
private readonly MenuItem _removeInventoryItem;
public ContextMenuIntegration(IChatGui chatGui, ItemCache itemCache, Configuration configuration,
IListManager listManager, ConfigWindow configWindow, IGameGui gameGui, IContextMenu contextMenu)
{
_chatGui = chatGui;
_itemCache = itemCache;
_configuration = configuration;
_listManager = listManager;
_configWindow = configWindow;
_gameGui = gameGui;
_contextMenu = contextMenu;
_addInventoryItem = new MenuItem
{
Prefix = (SeIconChar)57439,
PrefixColor = 52,
Name = "Add to Auto Discard List",
OnClicked = AddToDiscardList,
};
_removeInventoryItem = new MenuItem
{
Prefix = (SeIconChar)57439,
PrefixColor = 52,
Name = "Remove from Auto Discard List",
OnClicked = RemoveFromDiscardList,
};
_contextMenu.OnMenuOpened += MenuOpened;
}
private void MenuOpened(IMenuOpenedArgs args)
{
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))
.Append($"\ue05f ")
.Append(new UIForegroundPayload(0))
.Append($"Added ")
.Append(new UIForegroundPayload(52))
.Append(new ItemPayload(itemId))
.Append(_itemCache.GetItemName(itemId))
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" to Auto Discard List."));
}
}
private void RemoveFromDiscardList(IMenuItemClickedArgs args) =>
RemoveFromDiscardList(((MenuTargetInventory)args.Target).TargetItem!.Value.ItemId);
private void RemoveFromDiscardList(uint itemId)
{
if (_configWindow.RemoveFromDiscardList(itemId))
{
_chatGui.Print(new SeString(new UIForegroundPayload(52))
.Append($"\ue05f ")
.Append(new UIForegroundPayload(0))
.Append($"Removed ")
.Append(new UIForegroundPayload(52))
.Append(new ItemPayload(itemId))
.Append(_itemCache.GetItemName(itemId))
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.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()
{
_contextMenu.OnMenuOpened -= MenuOpened;
}
}

View File

@ -15,7 +15,6 @@
OutputPath="$(OutputPath)"
AssemblyName="$(AssemblyName)"
MakeZip="true"
VersionComponents="2"
Exclude="ARDiscard.deps.json;AutoRetainerAPI.pdb;ClickLib.pdb;ClickLib.xml;ECommons.pdb;ECommons.xml"/>
VersionComponents="2"/>
</Target>
</Project>

View File

@ -1,32 +0,0 @@
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,21 +0,0 @@
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

@ -1,12 +0,0 @@
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,208 +0,0 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
namespace ARDiscard.GameData;
internal sealed class InventoryUtils
{
private static readonly InventoryType[] DefaultInventoryTypes =
[
InventoryType.Inventory1,
InventoryType.Inventory2,
InventoryType.Inventory3,
InventoryType.Inventory4
];
private static readonly InventoryType[] MainHandOffHandInventoryTypes =
[
InventoryType.ArmoryMainHand,
InventoryType.ArmoryOffHand
];
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.ArmoryWrist,
InventoryType.ArmoryRings
];
private static readonly IReadOnlyList<uint> NoGearsetItems = new List<uint>();
private readonly Configuration _configuration;
private readonly ItemCache _itemCache;
private readonly IListManager _listManager;
private readonly IPluginLog _pluginLog;
public InventoryUtils(Configuration configuration, ItemCache itemCache, IListManager listManager, IPluginLog pluginLog)
{
_configuration = configuration;
_itemCache = itemCache;
_listManager = listManager;
_pluginLog = pluginLog;
}
public unsafe List<ItemWrapper> GetAllItemsToDiscard()
{
List<ItemWrapper> toDiscard = new List<ItemWrapper>();
Dictionary<uint, uint> itemCounts = new();
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (InventoryType inventoryType in DefaultInventoryTypes)
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, itemCounts, NoGearsetItems));
if (_configuration.Armoury.DiscardFromArmouryChest)
{
var gearsetItems = GetAllGearsetItems();
toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckMainHandOffHand, inventoryManager,
MainHandOffHandInventoryTypes, itemCounts, gearsetItems));
toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckLeftSideGear, inventoryManager,
LeftSideGearInventoryTypes, itemCounts, gearsetItems));
toDiscard.AddRange(GetArmouryItemsToDiscard(_configuration.Armoury.CheckRightSideGear, inventoryManager,
RightSideGearInventoryTypes, itemCounts, gearsetItems));
}
return toDiscard
.Where(x => itemCounts[x.InventoryItem->ItemId] < _configuration.IgnoreItemCountWhenAbove)
.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)
{
List<ItemWrapper> allItemsToDiscard = GetAllItemsToDiscard();
ItemWrapper? toDiscard = allItemsToDiscard.FirstOrDefault(x =>
itemFilter == null || itemFilter.ItemIds.Contains(x.InventoryItem->ItemId));
return toDiscard != null ? toDiscard.InventoryItem : null;
}
private unsafe ReadOnlyCollection<ItemWrapper> GetItemsToDiscard(InventoryManager* inventoryManager,
InventoryType inventoryType, Dictionary<uint, uint> itemCounts,
IReadOnlyList<uint>? gearsetItems)
{
List<ItemWrapper> toDiscard = new List<ItemWrapper>();
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 && item->ItemId != 0)
{
if (itemCounts.TryGetValue(item->ItemId, out uint itemCount))
itemCounts[item->ItemId] = itemCount + item->Quantity;
else
itemCounts[item->ItemId] = item->Quantity;
if (_listManager.IsBlacklisted(item->ItemId))
continue;
if (!_itemCache.TryGetItem(item->ItemId, out ItemCache.CachedItemInfo? itemInfo) ||
!itemInfo.CanBeDiscarded(_listManager))
continue; // no info, who knows what that item is
// skip gear if we're unable to load gearsets or it is used in a gearset
if (itemInfo.EquipSlotCategory > 0 && (gearsetItems == null || gearsetItems.Contains(item->ItemId)))
continue;
if (itemInfo is { EquipSlotCategory: > 0, CanBeBoughtFromCalamitySalvager: false } &&
itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel)
continue;
if (_configuration.IgnoreItemWithSignature && item->CrafterContentId != 0)
continue;
//PluginLog.Verbose($"{i} → {item->ItemID}");
if (_configuration.DiscardingItems.Contains(item->ItemId))
{
_pluginLog.Verbose(
$"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.AsReadOnly();
}
private unsafe List<uint>? GetAllGearsetItems()
{
var gearsetModule = RaptureGearsetModule.Instance();
if (gearsetModule == null)
return null;
List<uint> allGearsetItems = new();
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->GetItem(RaptureGearsetModule.GearsetItemIndex.MainHand),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.OffHand),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Head),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Body),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Hands),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Legs),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Feet),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Ears),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Neck),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.Wrists),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.RingLeft),
gearset->GetItem(RaptureGearsetModule.GearsetItemIndex.RingRight),
};
foreach (var gearsetItem in gearsetItems)
{
if (gearsetItem.ItemId != 0)
allGearsetItems.Add(gearsetItem.ItemId);
}
}
}
return allGearsetItems;
}
public unsafe void Discard(InventoryItem* item)
{
if (_listManager.IsBlacklisted(item->ItemId))
throw new ArgumentException($"Can't discard {item->ItemId}", nameof(item));
AgentInventoryContext.Instance()->DiscardItem(item, item->Container, item->Slot, 0);
}
public sealed unsafe class ItemWrapper
{
public required InventoryItem* InventoryItem { get; init; }
}
}

View File

@ -1,154 +0,0 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Lumina.Excel.GeneratedSheets;
namespace ARDiscard.GameData;
internal sealed class ItemCache
{
private readonly Dictionary<uint, CachedItemInfo> _items = new();
public ItemCache(IDataManager dataManager, ListManager listManager)
{
foreach (var item in dataManager.GetExcelSheet<Item>()!)
{
if (item.RowId == 0)
continue;
_items[item.RowId] = new CachedItemInfo
{
ItemId = item.RowId,
Name = item.Name.ToString(),
IconId = item.Icon,
ILvl = item.LevelItem.Row,
Rarity = item.Rarity,
IsUnique = item.IsUnique,
IsUntradable = item.IsUntradable,
IsIndisposable = item.IsIndisposable,
Level = item.LevelEquip,
UiCategory = item.ItemUICategory.Row,
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 bool TryGetItem(uint itemId, [NotNullWhen(true)] out CachedItemInfo? item)
=> _items.TryGetValue(itemId, out item);
public CachedItemInfo? GetItem(uint itemId)
{
if (_items.TryGetValue(itemId, out var item))
return item;
return null;
}
public string GetItemName(uint itemId)
{
if (_items.TryGetValue(itemId, out var item))
return item.Name;
return string.Empty;
}
public ushort GetItemIconId(uint itemId)
{
if (_items.TryGetValue(itemId, out var item))
return item.IconId;
return ushort.MinValue;
}
public sealed class CachedItemInfo
{
public required uint ItemId { get; init; }
public required string Name { get; init; }
public required ushort IconId { 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; }
/// <summary>
/// Whether this item can be discarded at all. "Discard" is greyed out e.g. for the preorder EXP earrings.
/// </summary>
public required bool IsIndisposable { get; init; }
public bool CanBeBoughtFromCalamitySalvager { get; set; }
public required uint UiCategory { 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

@ -1,10 +0,0 @@
using System.Collections.Generic;
namespace ARDiscard.GameData;
internal sealed class ItemFilter
{
public const ItemFilter? None = null;
public required List<uint> ItemIds { get; init; }
}

View File

@ -1,322 +0,0 @@
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

@ -1,8 +0,0 @@
namespace ARDiscard.GameData;
internal static class UiCategories
{
public const uint Unobtainable = 39;
public const uint Crystals = 59;
public const uint Currency = 100;
}

View File

@ -0,0 +1,67 @@
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);
}
}

View File

@ -1,336 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ARDiscard.GameData;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ImGuiNET;
using LLib.ImGui;
namespace ARDiscard.Windows;
internal sealed class ConfigWindow : LWindow
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly ItemCache _itemCache;
private readonly IListManager _listManager;
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly DiscardListTab _discardListTab;
private readonly ExcludedListTab _excludedListTab;
private List<(uint ItemId, string Name)>? _allItems;
public event EventHandler? DiscardNowClicked;
public event EventHandler? ConfigSaved;
public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache,
IListManager listManager, IClientState clientState, ICondition condition)
: base("Auto Discard###AutoDiscardConfig")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
_itemCache = itemCache;
_listManager = listManager;
_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),
};
_excludedListTab = new ExcludedListTab(this, itemCache, _configuration.BlacklistedItems, listManager);
_discardListTab = new DiscardListTab(this, itemCache, _configuration.DiscardingItems)
{
ExcludedTab = _excludedListTab,
};
}
public override void Draw()
{
bool runAfterVenture = _configuration.RunAfterVenture;
if (ImGui.Checkbox("[Global] Run automatically after AutoRetainer's venture", ref runAfterVenture))
{
_configuration.RunAfterVenture = runAfterVenture;
Save();
}
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize("Preview Discards").X -
ImGui.GetStyle().ItemSpacing.X);
ImGui.BeginDisabled(!_clientState.IsLoggedIn ||
!(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) ||
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))
{
_configuration.RunBeforeLogout = runBeforeLogout;
Save();
}
if (ImGui.BeginTabBar("AutoDiscardTabs"))
{
DrawDiscardList();
DrawExcludedCharacters();
DrawExcludedItems();
DrawExperimentalSettings();
ImGui.EndTabBar();
}
}
private void DrawDiscardList()
{
if (ImGui.BeginTabItem("Items to Discard"))
{
_discardListTab.Draw();
ImGui.EndTabItem();
}
}
private void DrawExcludedCharacters()
{
if (ImGui.BeginTabItem("Excluded Characters"))
{
if (_clientState is { IsLoggedIn: true, LocalContentId: > 0 })
{
string worldName = _clientState.LocalPlayer?.HomeWorld.GameData?.Name ?? "??";
ImGui.TextWrapped(
$"Current Character: {_clientState.LocalPlayer?.Name} @ {worldName} ({_clientState.LocalContentId:X})");
ImGui.Indent(30);
if (_configuration.ExcludedCharacters.Any(x => x.LocalContentId == _clientState.LocalContentId))
{
ImGui.TextColored(ImGuiColors.DalamudRed, "This character is currently excluded.");
if (ImGui.Button("Remove exclusion"))
{
_configuration.ExcludedCharacters.RemoveAll(
c => c.LocalContentId == _clientState.LocalContentId);
Save();
}
}
else
{
if (_configuration.RunAfterVenture || _configuration.RunBeforeLogout)
{
ImGui.TextColored(ImGuiColors.HealerGreen,
"This character is currently included (and will be post-processed in autoretainer).");
}
else
{
ImGui.TextColored(ImGuiColors.DalamudYellow,
"This character is currently included (but running post-processing is disabled globally)");
}
if (ImGui.Button("Exclude current character"))
{
_configuration.ExcludedCharacters.Add(new Configuration.CharacterInfo
{
LocalContentId = _clientState.LocalContentId,
CachedPlayerName = _clientState.LocalPlayer?.Name.ToString() ?? "??",
CachedWorldName = worldName,
});
Save();
}
}
ImGui.Unindent(30);
}
else
{
ImGui.TextColored(ImGuiColors.DalamudRed, "You are not logged in.");
}
ImGui.Separator();
ImGui.TextWrapped(
"Characters that won't run auto-cleanup after ventures (/discardall works for excluded characters)");
ImGui.Spacing();
ImGui.Indent(30);
if (_configuration.ExcludedCharacters.Count == 0)
{
ImGui.TextColored(ImGuiColors.DalamudGrey, "No excluded characters.");
}
else
{
foreach (var characterInfo in _configuration.ExcludedCharacters)
{
ImGui.Text(
$"{characterInfo.CachedPlayerName} @ {characterInfo.CachedWorldName} ({characterInfo.LocalContentId:X})");
}
}
ImGui.Unindent(30);
ImGui.EndTabItem();
}
}
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()
{
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 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;
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(_itemCache.MaxDungeonItemLevel, maximumItemLevel));
Save();
}
ImGui.Unindent(30);
ImGui.EndDisabled();
ImGui.Separator();
bool contextMenuEnabled = _configuration.ContextMenu.Enabled;
if (ImGui.Checkbox("Inventory context menu integration", ref contextMenuEnabled))
{
_configuration.ContextMenu.Enabled = contextMenuEnabled;
Save();
}
ImGui.BeginDisabled(!contextMenuEnabled);
ImGui.Indent(30);
bool contextMenuOnlyWhenConfigIsOpen = _configuration.ContextMenu.OnlyWhenConfigIsOpen;
if (ImGui.Checkbox("Only add menu entries while config window is open",
ref contextMenuOnlyWhenConfigIsOpen))
{
_configuration.ContextMenu.OnlyWhenConfigIsOpen = contextMenuOnlyWhenConfigIsOpen;
Save();
}
ImGui.Unindent(30);
ImGui.EndDisabled();
ImGui.Separator();
ImGui.SetNextItemWidth(ImGuiHelpers.GlobalScale * 100);
int ignoreItemCountWhenAbove = (int)_configuration.IgnoreItemCountWhenAbove;
if (ImGui.InputInt("Ignore stacks with >= this number of items", ref ignoreItemCountWhenAbove))
{
_configuration.IgnoreItemCountWhenAbove = (uint)Math.Max(2, ignoreItemCountWhenAbove);
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();
bool groupPreviewByCategory = _configuration.Preview.GroupByCategory;
if (ImGui.Checkbox("Group items in 'Preview' by category", ref groupPreviewByCategory))
{
_configuration.Preview.GroupByCategory = groupPreviewByCategory;
Save();
}
bool showIconsInPreview = _configuration.Preview.ShowIcons;
if (ImGui.Checkbox("Show icons in 'Preview'", ref showIconsInPreview))
{
_configuration.Preview.ShowIcons = showIconsInPreview;
Save();
}
ImGui.EndTabItem();
}
}
internal List<(uint ItemId, string Name)> EnsureAllItemsLoaded()
{
if (_allItems == null)
{
_allItems = _itemCache.AllItems
.Where(x => x.CanBeDiscarded(_listManager, false))
.Select(x => (x.ItemId, x.Name.ToString()))
.ToList();
}
return _allItems;
}
internal void Save()
{
_configuration.DiscardingItems = _discardListTab.ToSavedItems().ToList();
_configuration.BlacklistedItems = _excludedListTab.ToSavedItems().ToList();
_pluginInterface.SavePluginConfig(_configuration);
ConfigSaved?.Invoke(this, EventArgs.Empty);
}
internal bool AddToDiscardList(uint itemId) => _discardListTab.AddToDiscardList(itemId);
internal bool RemoveFromDiscardList(uint itemId) => _discardListTab.RemoveFromDiscardList(itemId);
public bool CanItemBeConfigured(uint itemId)
{
return EnsureAllItemsLoaded().SingleOrDefault(x => x.ItemId == itemId).ItemId == itemId;
}
}

View File

@ -1,55 +0,0 @@
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,201 +0,0 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ARDiscard.GameData;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface.Internal;
using Dalamud.Interface.Textures;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace ARDiscard.Windows;
internal sealed class DiscardWindow : LWindow
{
private readonly InventoryUtils _inventoryUtils;
private readonly ItemCache _itemCache;
private readonly IconCache _iconCache;
private readonly IClientState _clientState;
private readonly ICondition _condition;
private readonly Configuration _configuration;
private List<SelectableItem> _displayedItems = new();
public event EventHandler? OpenConfigurationClicked;
public event EventHandler<ItemFilter>? DiscardAllClicked;
public DiscardWindow(InventoryUtils inventoryUtils, ItemCache itemCache, IconCache iconCache,
IClientState clientState, ICondition condition, Configuration configuration)
: base("Discard Items###AutoDiscardDiscard")
{
_inventoryUtils = inventoryUtils;
_itemCache = itemCache;
_iconCache = iconCache;
_clientState = clientState;
_condition = condition;
_configuration = configuration;
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; }
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
{
if (_configuration.Preview.GroupByCategory)
{
foreach (var category in _displayedItems.OrderBy(x => x.UiCategory)
.GroupBy(x => new { x.UiCategory, x.UiCategoryName }))
{
ImGui.Text($"{category.Key.UiCategoryName}");
ImGui.Indent();
foreach (var displayedItem in category)
DrawItem(displayedItem);
ImGui.Unindent();
}
}
else
{
foreach (var displayedItem in _displayedItems)
DrawItem(displayedItem);
}
}
}
ImGui.EndDisabled();
ImGui.EndChild();
ImGui.BeginDisabled(OpenConfigurationClicked == null);
if (ImGui.Button("Open Configuration"))
OpenConfigurationClicked!.Invoke(this, EventArgs.Empty);
ImGui.EndDisabled();
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize("Discard all selected items").X -
ImGui.GetStyle().ItemSpacing.X);
ImGui.BeginDisabled(Locked ||
!_clientState.IsLoggedIn ||
!(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) ||
!_displayedItems.Any(x => x.Selected) ||
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();
}
private void DrawItem(SelectableItem displayedItem)
{
if (_configuration.Preview.ShowIcons)
{
ISharedImmediateTexture icon = _iconCache.GetIcon(displayedItem.IconId);
if (icon.TryGetWrap(out IDalamudTextureWrap? wrap, out _))
{
ImGui.Image(wrap.ImGuiHandle, new Vector2(23, 23));
ImGui.SameLine();
ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3);
wrap.Dispose();
}
}
if (ImGui.Selectable(displayedItem.ToString(), displayedItem.Selected))
displayedItem.Selected = !displayedItem.Selected;
}
public override void OnOpen() => RefreshInventory(false);
public override void OnClose() => _displayedItems.Clear();
public unsafe void RefreshInventory(bool keepSelected)
{
if (!IsOpen)
return;
List<uint> notSelected = new();
if (keepSelected)
{
notSelected.AddRange(_displayedItems
.Where(x => !x.Selected)
.Select(x => x.ItemId));
}
_displayedItems = _inventoryUtils.GetAllItemsToDiscard()
.GroupBy(x => new
{
ItemId = x.InventoryItem->ItemId,
ItemInfo = _itemCache.GetItem(x.InventoryItem->ItemId),
})
.Where(x => x.Key.ItemInfo != null)
.Select(x => new SelectableItem
{
ItemId = x.Key.ItemId,
Name = x.Key.ItemInfo!.Name,
IconId = x.Key.ItemInfo!.IconId,
Quantity = x.Sum(y => y.InventoryItem->Quantity),
UiCategory = x.Key.ItemInfo!.UiCategory,
UiCategoryName = x.Key.ItemInfo!.UiCategoryName,
Selected = !notSelected.Contains(x.Key.ItemId),
})
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private sealed class SelectableItem
{
public required uint ItemId { get; init; }
public required string Name { get; init; }
public required ushort IconId { get; init; }
public required long Quantity { get; init; }
public required uint UiCategory { get; init; }
public required string UiCategoryName { get; init; }
public required bool Selected { get; set; }
public override string ToString()
{
if (Quantity > 1)
return $"{Name} ({Quantity}x)";
return Name;
}
}
public void Login() => RefreshInventory(false);
public void Logout() => _displayedItems.Clear();
}

View File

@ -1,35 +0,0 @@
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

@ -1,161 +0,0 @@
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,95 +1,12 @@
{
"version": 1,
"dependencies": {
"net8.0-windows7.0": {
"net7.0-windows7.0": {
"DalamudPackager": {
"type": "Direct",
"requested": "[2.1.13, )",
"resolved": "2.1.13",
"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": {
"type": "Project",
"dependencies": {
"ECommons": "[2.2.0.2, )"
}
},
"ecommons": {
"type": "Project"
},
"llib": {
"type": "Project",
"dependencies": {
"DalamudPackager": "[2.1.13, )"
}
"requested": "[2.1.11, )",
"resolved": "2.1.11",
"contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw=="
}
}
}

@ -1 +0,0 @@
Subproject commit a63c8e7154e272374ffa03d5c801736d4229e38a

@ -1 +0,0 @@
Subproject commit 38080f2a3733aa19b6928f4d2984fac7b9a7fab7

1
LLib

@ -1 +0,0 @@
Subproject commit 7027d291efbbff6a55944dd521d3907210ddecbe

View File

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