Compare commits

..

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

28 changed files with 379 additions and 2489 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,79 @@
<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.1</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.4\</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,65 +1,55 @@
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.ClientState.Conditions;
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, Condition condition)
{
ArgumentNullException.ThrowIfNull(dataManager);
ItemCache itemCache = new ItemCache(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)
{
@ -73,39 +63,26 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
{
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);
_inventoryUtils = new InventoryUtils(_configuration, itemCache);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenMainUi += OpenDiscardUi;
_pluginInterface.UiBuilder.OpenConfigUi += OpenConfigUi;
_discardWindow = new(_inventoryUtils, itemCache, _iconCache, clientState, condition, _configuration);
_discardWindow = new(_inventoryUtils, itemCache, clientState, condition);
_windowSystem.AddWindow(_discardWindow);
_configWindow = new(_pluginInterface, _configuration, itemCache, listManager, clientState, condition);
_configWindow = new(_pluginInterface, _configuration, itemCache, clientState, condition);
_windowSystem.AddWindow(_configWindow);
_configWindow.DiscardNowClicked += (_, _) => OpenDiscardWindow(string.Empty, string.Empty);
_configWindow.ConfigSaved += (_, _) => _discardWindow.RefreshInventory(true);
_discardWindow.OpenConfigurationClicked += (_, _) => OpenConfigUi();
_discardWindow.DiscardAllClicked += (_, filter) =>
{
_taskManager?.Abort();
_taskManager?.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, filter));
};
_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;
@ -115,16 +92,7 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
_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 +104,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)
{
_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,13 +126,11 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
private void DoRetainerPostProcess(string retainerName)
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Retainer, ItemFilter.None));
}
private void DoCharacterPostProcess()
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.Character, ItemFilter.None));
}
@ -177,38 +143,35 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
private void DiscardAll(string command, string arguments)
{
_taskManager.Abort();
_taskManager.Enqueue(() => DiscardNextItem(PostProcessType.ManuallyStarted, ItemFilter.None));
}
private void OpenDiscardWindow(string command, string arguments) => OpenDiscardUi();
private void OpenDiscardUi()
private void OpenDiscardWindow(string command, string arguments)
{
_discardWindow.IsOpen = !_discardWindow.IsOpen;
}
private unsafe void DiscardNextItem(PostProcessType type, ItemFilter? itemFilter)
{
_pluginLog.Information($"DiscardNextItem (type = {type})");
PluginLog.Information($"DiscardNextItem (type = {type})");
_discardWindow.Locked = true;
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
if (nextItem == null)
{
_pluginLog.Information("No item to discard found");
PluginLog.Information($"No item to discard found");
FinishDiscarding(type);
}
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.DelayNext(20);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, inventoryType, slot));
}
}
@ -219,11 +182,11 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
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.DelayNext(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, inventoryType, slot));
}
else
@ -231,21 +194,21 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
if (nextItem == null)
{
_pluginLog.Information("Addon is not visible, but next item is also no longer set");
PluginLog.Information("Addon is not visible, but next item is also no longer set");
FinishDiscarding(type);
}
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.DelayNext(100);
_taskManager.Enqueue(() => ConfirmDiscardItem(type, itemFilter, 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.DelayNext(100);
_taskManager.Enqueue(() => DiscardNextItem(type, itemFilter));
}
}
@ -257,28 +220,27 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
InventoryItem* nextItem = _inventoryUtils.GetNextItemToDiscard(itemFilter);
if (nextItem == null)
{
_pluginLog.Information($"Continuing after discard: no next item (type = {type})");
PluginLog.Information($"Continuing after discard: no next item (type = {type})");
FinishDiscarding(type);
}
else if (nextItem->Container == inventoryType && nextItem->Slot == slot)
{
if (_cancelDiscardAfter < DateTime.Now)
{
_pluginLog.Information("No longer waiting for plugin to pop up, assume discard failed");
PluginLog.Information("No longer waiting for plugin to pop up, assume discard failed");
FinishDiscarding(type, "Discarding probably failed due to an error.");
}
else
{
_pluginLog.Verbose(
$"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}");
_taskManager.EnqueueDelay(20);
PluginLog.Information($"ContinueAfterDiscard: Waiting for server response until {_cancelDiscardAfter}");
_taskManager.DelayNext(20);
_taskManager.Enqueue(() => ContinueAfterDiscard(type, itemFilter, 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, itemFilter));
}
}
@ -309,34 +271,31 @@ public sealed class AutoDiscardPlogon : IDalamudPlugin
_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;
}

View File

@ -3,46 +3,28 @@ using Dalamud.Configuration;
namespace ARDiscard;
internal sealed class Configuration : IPluginConfiguration
public sealed 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 ulong LocalContentId { get; set; }
public string? CachedPlayerName { get; set; }
public string? CachedWorldName { 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 bool DiscardFromArmouryChest { get; set; } = false;
public bool CheckLeftSideGear { get; set; } = false;
public bool CheckRightSideGear { get; set; } = false;
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;
}
}

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

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace ARDiscard.GameData;
public static class InternalConfiguration
{
public static IReadOnlyList<uint> BlacklistedItems = new List<uint>
{
2820, // red onion helm
}.AsReadOnly();
}

View File

@ -1,148 +1,131 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Dalamud.Plugin.Services;
using Dalamud.Data;
using Dalamud.Hooking;
using Dalamud.Logging;
using Dalamud.Utility.Signatures;
using FFXIVClientStructs.FFXIV.Client.Game;
using FFXIVClientStructs.FFXIV.Client.UI.Agent;
using FFXIVClientStructs.FFXIV.Client.UI.Misc;
using Lumina.Excel.GeneratedSheets;
namespace ARDiscard.GameData;
internal sealed class InventoryUtils
public class InventoryUtils : IDisposable
{
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.ArmoryHands,
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)
private unsafe delegate void DiscardItemDelegate(AgentInventoryContext* inventoryManager, InventoryItem* itemSlot,
InventoryType inventory, int slot, uint addonId, int position = -1);
[Signature("E8 ?? ?? ?? ?? E9 ?? ?? ?? ?? 48 8D 0D ?? ?? ?? ?? E8 ?? ?? ?? ?? 48 85 C0 74 ?? 0F B7 48")]
private DiscardItemDelegate _discardItem = null!;
public InventoryUtils(Configuration configuration, ItemCache itemCache)
{
_configuration = configuration;
_itemCache = itemCache;
_listManager = listManager;
_pluginLog = pluginLog;
SignatureHelper.Initialise(this);
}
public void Dispose()
{
}
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));
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, false));
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));
if (_configuration.Armoury.CheckLeftSideGear)
{
foreach (InventoryType inventoryType in LeftSideGearInventoryTypes)
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, true));
}
if (_configuration.Armoury.CheckRightSideGear)
{
foreach (InventoryType inventoryType in RightSideGearInventoryTypes)
toDiscard.AddRange(GetItemsToDiscard(inventoryManager, inventoryType, true));
}
}
return toDiscard
.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();
return toDiscard;
}
public unsafe InventoryItem* GetNextItemToDiscard(ItemFilter? itemFilter)
{
List<ItemWrapper> allItemsToDiscard = GetAllItemsToDiscard();
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;
}
private unsafe ReadOnlyCollection<ItemWrapper> GetItemsToDiscard(InventoryManager* inventoryManager,
InventoryType inventoryType, Dictionary<uint, uint> itemCounts,
IReadOnlyList<uint>? gearsetItems)
private unsafe IReadOnlyList<ItemWrapper> GetItemsToDiscard(InventoryManager* inventoryManager,
InventoryType inventoryType, bool doGearChecks)
{
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 (item != null)
{
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))
if (InternalConfiguration.BlacklistedItems.Contains(item->ItemID))
continue;
if (!_itemCache.TryGetItem(item->ItemId, out ItemCache.CachedItemInfo? itemInfo) ||
!itemInfo.CanBeDiscarded(_listManager))
continue; // no info, who knows what that item is
if (doGearChecks)
{
if (IsItemPartOfGearset(item->ItemID))
continue;
// 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;
ItemCache.CachedItemInfo? itemInfo = _itemCache.GetItem(item->ItemID);
if (itemInfo == null)
continue; // no info, who knows what that item is
if (itemInfo is { EquipSlotCategory: > 0, CanBeBoughtFromCalamitySalvager: false } &&
itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel)
continue;
if (_configuration.IgnoreItemWithSignature && item->CrafterContentId != 0)
continue;
if (itemInfo.ILvl >= _configuration.Armoury.MaximumGearItemLevel)
continue;
}
//PluginLog.Verbose($"{i} → {item->ItemID}");
if (_configuration.DiscardingItems.Contains(item->ItemId))
if (_configuration.DiscardingItems.Contains(item->ItemID))
{
_pluginLog.Verbose(
$"Found item {item->ItemId} to discard in inventory {inventoryType} in slot {i}");
PluginLog.Information(
$"Found item {item->ItemID} to discard in inventory {inventoryType} in slot {i}");
toDiscard.Add(new ItemWrapper { InventoryItem = item });
}
}
@ -152,16 +135,15 @@ internal sealed class InventoryUtils
}
}
return toDiscard.AsReadOnly();
return toDiscard;
}
private unsafe List<uint>? GetAllGearsetItems()
private unsafe bool IsItemPartOfGearset(uint searchForItemId)
{
var gearsetModule = RaptureGearsetModule.Instance();
if (gearsetModule == null)
return null;
return true; // can't check gearsets, pretend everything is part of one
List<uint> allGearsetItems = new();
for (int i = 0; i < 100; ++i)
{
var gearset = gearsetModule->GetGearset(i);
@ -169,36 +151,33 @@ internal sealed class InventoryUtils
{
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),
gearset->MainHand,
gearset->OffHand,
gearset->Head,
gearset->Body,
gearset->Hands,
gearset->Legs,
gearset->Feet,
gearset->Ears,
gearset->Neck,
gearset->Wrists,
gearset->RingRight,
gearset->RightLeft, // why is this called RightLeft
};
foreach (var gearsetItem in gearsetItems)
{
if (gearsetItem.ItemId != 0)
allGearsetItems.Add(gearsetItem.ItemId);
if (gearsetItem.ItemID == searchForItemId)
return true;
}
}
}
return allGearsetItems;
return false;
}
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);
_discardItem(AgentInventoryContext.Instance(), item, item->Container, item->Slot, 0);
}
public sealed unsafe class ItemWrapper

View File

@ -1,17 +1,14 @@
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Dalamud.Plugin.Services;
using Lumina.Excel;
using Dalamud.Data;
using Lumina.Excel.GeneratedSheets;
namespace ARDiscard.GameData;
internal sealed class ItemCache
public sealed class ItemCache
{
private readonly Dictionary<uint, CachedItemInfo> _items = new();
public ItemCache(IDataManager dataManager, ListManager listManager)
public ItemCache(DataManager dataManager)
{
foreach (var item in dataManager.GetExcelSheet<Item>()!)
{
@ -22,83 +19,19 @@ internal sealed class ItemCache
{
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 CachedItemInfo? GetItem(uint itemId) => _items[itemId];
public string GetItemName(uint itemId)
{
@ -107,48 +40,15 @@ internal sealed class ItemCache
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

@ -2,9 +2,9 @@
namespace ARDiscard.GameData;
internal sealed class ItemFilter
public class ItemFilter
{
public const ItemFilter? None = null;
public static 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,6 +1,6 @@
namespace ARDiscard.GameData;
internal static class UiCategories
public static class UiCategories
{
public const uint Unobtainable = 39;
public const uint Crystals = 59;

View File

@ -3,40 +3,42 @@ using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ARDiscard.GameData;
using Dalamud.Game.ClientState;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using ImGuiNET;
using LLib.ImGui;
using Condition = Dalamud.Game.ClientState.Conditions.Condition;
namespace ARDiscard.Windows;
internal sealed class ConfigWindow : LWindow
public sealed class ConfigWindow : Window
{
private readonly IDalamudPluginInterface _pluginInterface;
private readonly DalamudPluginInterface _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 readonly ClientState _clientState;
private readonly Condition _condition;
private string _itemName = string.Empty;
private List<(uint ItemId, string Name)>? _allItems;
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 event EventHandler? DiscardNowClicked;
public event EventHandler? ConfigSaved;
public ConfigWindow(IDalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache,
IListManager listManager, IClientState clientState, ICondition condition)
public ConfigWindow(DalamudPluginInterface pluginInterface, Configuration configuration, ItemCache itemCache,
ClientState clientState, Condition condition)
: base("Auto Discard###AutoDiscardConfig")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
_itemCache = itemCache;
_listManager = listManager;
_clientState = clientState;
_condition = condition;
@ -49,11 +51,8 @@ internal sealed class ConfigWindow : LWindow
MaximumSize = new Vector2(9999, 9999),
};
_excludedListTab = new ExcludedListTab(this, itemCache, _configuration.BlacklistedItems, listManager);
_discardListTab = new DiscardListTab(this, itemCache, _configuration.DiscardingItems)
{
ExcludedTab = _excludedListTab,
};
_discarding.AddRange(_configuration.DiscardingItems
.Select(x => (x, itemCache.GetItemName(x))).ToList());
}
public override void Draw()
@ -65,12 +64,8 @@ internal sealed class ConfigWindow : LWindow
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]) ||
ImGui.SameLine(ImGui.GetWindowWidth() - 115 * ImGuiHelpers.GlobalScale);
ImGui.BeginDisabled(!_clientState.IsLoggedIn || !_condition[ConditionFlag.NormalConditions] ||
DiscardNowClicked == null);
if (ImGui.Button("Preview Discards"))
DiscardNowClicked!.Invoke(this, EventArgs.Empty);
@ -87,7 +82,6 @@ internal sealed class ConfigWindow : LWindow
{
DrawDiscardList();
DrawExcludedCharacters();
DrawExcludedItems();
DrawExperimentalSettings();
ImGui.EndTabBar();
@ -98,7 +92,91 @@ internal sealed class ConfigWindow : LWindow
{
if (ImGui.BeginTabItem("Items to Discard"))
{
_discardListTab.Draw();
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();
}
}
@ -180,19 +258,6 @@ internal sealed class ConfigWindow : 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()
{
if (ImGui.BeginTabItem("Experimental Settings"))
@ -207,14 +272,6 @@ internal sealed class ConfigWindow : LWindow
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))
{
@ -234,103 +291,44 @@ internal sealed class ConfigWindow : LWindow
if (ImGui.InputInt("Ignore items >= this ilvl (Armoury Chest only)",
ref maximumItemLevel))
{
_configuration.Armoury.MaximumGearItemLevel =
Math.Max(0, Math.Min(_itemCache.MaxDungeonItemLevel, maximumItemLevel));
_configuration.Armoury.MaximumGearItemLevel = Math.Max(0, Math.Min(625, 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()
private void UpdateResults()
{
if (_allItems == null)
if (string.IsNullOrEmpty(_itemName))
_searchResults = new();
else
{
_allItems = _itemCache.AllItems
.Where(x => x.CanBeDiscarded(_listManager, false))
.Select(x => (x.ItemId, x.Name.ToString()))
if (_allItems == null)
{
_allItems = _itemCache.AllItems
.Where(x => !x.IsUnique && !x.IsUntradable)
.Where(x => x.UiCategory != UiCategories.Currency && x.UiCategory != UiCategories.Crystals &&
x.UiCategory != UiCategories.Unobtainable)
.Select(x => (x.ItemId, 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();
}
return _allItems;
}
internal void Save()
private void Save()
{
_configuration.DiscardingItems = _discardListTab.ToSavedItems().ToList();
_configuration.BlacklistedItems = _excludedListTab.ToSavedItems().ToList();
_configuration.DiscardingItems = _discarding.Select(x => x.ItemId).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,45 +1,36 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using ARDiscard.GameData;
using Dalamud.Game.ClientState;
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 Dalamud.Interface;
using Dalamud.Interface.Windowing;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace ARDiscard.Windows;
internal sealed class DiscardWindow : LWindow
public sealed class DiscardWindow : Window
{
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 readonly ClientState _clientState;
private readonly Condition _condition;
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")
public DiscardWindow(InventoryUtils inventoryUtils, ItemCache itemCache, ClientState clientState,
Condition condition)
: base("Discard Items")
{
_inventoryUtils = inventoryUtils;
_itemCache = itemCache;
_iconCache = iconCache;
_clientState = clientState;
_condition = condition;
_configuration = configuration;
Size = new Vector2(600, 400);
SizeCondition = ImGuiCond.FirstUseEver;
@ -51,7 +42,7 @@ internal sealed class DiscardWindow : LWindow
};
}
public bool Locked { get; set; }
public bool Locked { get; set; } = false;
public override void Draw()
{
@ -70,22 +61,10 @@ internal sealed class DiscardWindow : LWindow
}
else
{
if (_configuration.Preview.GroupByCategory)
foreach (var displayedItem in _displayedItems)
{
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);
if (ImGui.Selectable(displayedItem.ToString(), displayedItem.Selected))
displayedItem.Selected = !displayedItem.Selected;
}
}
}
@ -98,15 +77,11 @@ internal sealed class DiscardWindow : LWindow
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.SameLine(ImGui.GetWindowWidth() - 160 * ImGuiHelpers.GlobalScale);
ImGui.BeginDisabled(Locked ||
!_clientState.IsLoggedIn ||
!(_condition[ConditionFlag.NormalConditions] || _condition[ConditionFlag.Mounted]) ||
!_displayedItems.Any(x => x.Selected) ||
!_condition[ConditionFlag.NormalConditions] ||
_displayedItems.Count(x => x.Selected) == 0 ||
DiscardAllClicked == null);
if (ImGui.Button("Discard all selected items"))
{
@ -119,25 +94,6 @@ internal sealed class DiscardWindow : LWindow
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();
@ -156,23 +112,14 @@ internal sealed class DiscardWindow : LWindow
}
_displayedItems = _inventoryUtils.GetAllItemsToDiscard()
.GroupBy(x => new
{
ItemId = x.InventoryItem->ItemId,
ItemInfo = _itemCache.GetItem(x.InventoryItem->ItemId),
})
.Where(x => x.Key.ItemInfo != null)
.GroupBy(x => x.InventoryItem->ItemID)
.Select(x => new SelectableItem
{
ItemId = x.Key.ItemId,
Name = x.Key.ItemInfo!.Name,
IconId = x.Key.ItemInfo!.IconId,
ItemId = x.Key,
Name = _itemCache.GetItemName(x.Key),
Quantity = x.Sum(y => y.InventoryItem->Quantity),
UiCategory = x.Key.ItemInfo!.UiCategory,
UiCategoryName = x.Key.ItemInfo!.UiCategoryName,
Selected = !notSelected.Contains(x.Key.ItemId),
Selected = !notSelected.Contains(x.Key),
})
.OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
@ -180,11 +127,8 @@ internal sealed class DiscardWindow : LWindow
{
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 bool Selected { get; set; } = true;
public override string ToString()
{
@ -195,7 +139,7 @@ internal sealed class DiscardWindow : LWindow
}
}
public void Login() => RefreshInventory(false);
public void Login(object? sender, EventArgs e) => RefreshInventory(false);
public void Logout() => _displayedItems.Clear();
public void Logout(object? sender, EventArgs e) => _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
}
}