Compare commits

..

36 Commits
v2.2 ... master

Author SHA1 Message Date
a992a841e8
Show when items have unobtained folklore books in 'Locked Items' tab 2024-10-28 22:43:19 +01:00
23e29c4cc6
Fix 2024-10-28 21:33:32 +01:00
e834160cd3
Show last updated time on Inventory tab 2024-08-02 15:51:49 +02:00
9250044c60
Retainers can now be disabled if they're under the minimum level (but not enabled) 2024-07-28 17:58:45 +02:00
8f6b38a3e8
Add inventory tab 2024-07-23 17:07:25 +02:00
9efb28cbb0
Add a tooltip for certain shard/crystal configurations 2024-07-22 10:38:10 +02:00
eeb02156a0 Add Retainer crystals to tracked inventories (#3) 2024-07-22 08:21:08 +00:00
OhKannaDuh
0ed33ea9a9 Add Retainer crystals to tracked inventories 2024-07-22 01:09:12 +01:00
c30abb79ab
Fix button alignment when no drag/drop button is visible 2024-07-19 19:23:03 +02:00
89951227af
Support drag & drop to redorder lists 2024-07-18 19:38:43 +02:00
44f44bce8a
Split UI into different classes 2024-07-18 18:36:56 +02:00
c522574ab9
Update venture list item sorting 2024-07-18 18:00:42 +02:00
ec56f22e36
Don't attempt to clear clipboard text after paste 2024-07-18 16:29:32 +02:00
9ecbd1e3b2
API 10 2024-07-03 19:08:40 +02:00
0425d446a1
Don't assign ventures if AR is set to collect only
This is only effective when the 'Operation' under Settings → General is
set to 'Collect', it ignores any hotkeys that would (normally) modify
how AR operates.
2024-05-05 11:38:51 +02:00
8a3a6399b2
Make assignment chat message optional 2024-04-26 20:02:23 +02:00
b66b4689b8
Improve icon size calculations 2024-03-27 18:35:40 +01:00
bc65bfe24c
Fix scaling/spacing issues 2024-03-27 09:30:17 +01:00
238b58d555
NET 8 2024-03-27 08:26:14 +01:00
ec256ac090
Add 'Check Retainer Inventory' for lists to keep in stock 2024-01-24 16:25:13 +01:00
e78cd642cd
Fix issue where non-existent retainers could cause plugin to not work properly 2024-01-20 02:38:50 +01:00
37e9f29d41
Show warning icon for icons that are on auto-discard list 2024-01-13 23:36:24 +01:00
a722f6a303
Highlight current player when viewing 'Locked items', but grouped by venture instead of character 2024-01-07 18:54:36 +01:00
31cbb118a4
Use RetainerId instead of Name as id trait 2024-01-07 18:31:32 +01:00
9477be08d7
Don't fetch more items when stocking up if at the exact item count 2023-11-20 12:33:13 +01:00
75885e004a
Bump version/recompile 2023-11-14 20:13:57 +01:00
1002e85eb4
Update header icon logic for Dalamud changes 2023-11-09 11:40:46 +01:00
ba40a916b3
'Add venture' filter now adds the first venture when pressing enter 2023-11-04 01:47:20 +01:00
cfc41a5a61
Import from clipboard, short notation for adding new items 2023-11-02 02:28:26 +01:00
3138d6fc5f
Use AutoRetainerAPI as submodule 2023-11-01 10:26:40 +01:00
6b432bfc77
Move IconCache to LLib 2023-11-01 10:24:49 +01:00
8b11b67c96
Add a configurable minimum venture count, under which only quick ventures are assigned 2023-10-19 01:22:57 +02:00
65af6814df
Add UI/sorting options 2023-10-17 12:51:06 +02:00
297544128d
Set missing popup flag 2023-10-17 12:12:44 +02:00
26d8a005ba
Update submodule URL 2023-10-14 00:00:31 +02:00
a151105520
Fix retainers repeating the last venture when out of tasks
We now manually set them to execute a Quick Venture when nothing else is
left to do, instead of relying on AR's behavior.
2023-10-13 19:06:17 +02:00
28 changed files with 3461 additions and 1169 deletions

8
.gitmodules vendored
View File

@ -1,3 +1,9 @@
[submodule "LLib"] [submodule "LLib"]
path = LLib path = LLib
url = git@git.carvel.li:liza/LLib.git 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

@ -4,6 +4,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARCo
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{C00249D7-E550-4A3F-937B-D938D1D46B8A}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{C00249D7-E550-4A3F-937B-D938D1D46B8A}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoRetainerAPI", "AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj", "{325D077C-A47F-41D7-AA63-0FD3B4037C0B}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ECommons", "ECommons\ECommons\ECommons.csproj", "{A3EDB8A3-1BB0-4E1A-A957-6F60D4C85290}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@ -18,5 +22,13 @@ Global
{C00249D7-E550-4A3F-937B-D938D1D46B8A}.Debug|Any CPU.Build.0 = Debug|Any CPU {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.ActiveCfg = Release|Any CPU {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.Build.0 = Release|Any CPU {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.Build.0 = Release|Any CPU
{325D077C-A47F-41D7-AA63-0FD3B4037C0B}.Debug|Any CPU.ActiveCfg = Debug|x64
{325D077C-A47F-41D7-AA63-0FD3B4037C0B}.Debug|Any CPU.Build.0 = Debug|x64
{325D077C-A47F-41D7-AA63-0FD3B4037C0B}.Release|Any CPU.ActiveCfg = Release|x64
{325D077C-A47F-41D7-AA63-0FD3B4037C0B}.Release|Any CPU.Build.0 = Release|x64
{A3EDB8A3-1BB0-4E1A-A957-6F60D4C85290}.Debug|Any CPU.ActiveCfg = Debug|x64
{A3EDB8A3-1BB0-4E1A-A957-6F60D4C85290}.Debug|Any CPU.Build.0 = Debug|x64
{A3EDB8A3-1BB0-4E1A-A957-6F60D4C85290}.Release|Any CPU.ActiveCfg = Release|x64
{A3EDB8A3-1BB0-4E1A-A957-6F60D4C85290}.Release|Any CPU.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal

View File

@ -0,0 +1,7 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=Allagan/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gatherbtn/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gatherfish/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=gathermin/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=relog/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Teamcraft/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

1017
ARControl/.editorconfig Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -4,5 +4,5 @@
"Punchline": "Better AutoRetainer Venture Planner", "Punchline": "Better AutoRetainer Venture Planner",
"Description": "", "Description": "",
"RepoUrl": "https://git.carvel.li/liza/ARControl", "RepoUrl": "https://git.carvel.li/liza/ARControl",
"IconUrl": "https://git.carvel.li/liza/plugin-repo/raw/branch/master/dist/ARControl.png" "IconUrl": "https://plugins.carvel.li/icons/ARControl.png"
} }

View File

@ -38,14 +38,63 @@ partial class AutoRetainerControlPlugin
save = true; save = true;
} }
List<string> seenRetainers = new(); if (character.Ventures != offlineCharacterData.Ventures)
foreach (var retainerData in offlineCharacterData.RetainerData)
{ {
var retainer = character.Retainers.SingleOrDefault(x => x.Name == retainerData.Name); character.Ventures = offlineCharacterData.Ventures;
save = true;
}
if (_clientState.LocalContentId == registeredCharacterId)
{
var unlockedFolkloreBooks = _gameCache.FolkloreBooks.Values.Where(x => x.IsUnlocked()).Select(x => x.ItemId).ToHashSet();
if (character.UnlockedFolkloreBooks != unlockedFolkloreBooks)
{
character.UnlockedFolkloreBooks = unlockedFolkloreBooks;
save = true;
}
}
// remove retainers without name
save |= character.Retainers.RemoveAll(x => string.IsNullOrEmpty(x.Name)) > 0;
// migrate legacy retainers
foreach (var legacyRetainer in character.Retainers.Where(x => x.RetainerContentId == 0))
{
_pluginLog.Information($"Migrating retainer '{legacyRetainer.Name}' (char: {character})");
var retainerData =
offlineCharacterData.RetainerData.SingleOrDefault(x => legacyRetainer.Name == x.Name);
if (retainerData != null)
{
_pluginLog.Information(
$"Assigning contentId {retainerData.RetainerID} to retainer {retainerData.Name}");
legacyRetainer.RetainerContentId = retainerData.RetainerID;
save = true;
}
}
var retainersWithoutContentId = character.Retainers.Where(c => c.RetainerContentId == 0).ToList();
if (retainersWithoutContentId.Count > 0)
{
foreach (var retainer in retainersWithoutContentId)
{
_pluginLog.Warning($"Removing retainer {retainer.Name} without contentId");
character.Retainers.Remove(retainer);
}
save = true;
}
List<ulong> unknownRetainerIds = offlineCharacterData.RetainerData.Select(x => x.RetainerID).Where(x => x != 0).ToList();
foreach (var retainerData in offlineCharacterData.RetainerData.Where(x => !string.IsNullOrEmpty(x.Name)))
{
unknownRetainerIds.Remove(retainerData.RetainerID);
var retainer = character.Retainers.SingleOrDefault(x => x.RetainerContentId == retainerData.RetainerID);
if (retainer == null) if (retainer == null)
{ {
retainer = new Configuration.RetainerConfiguration retainer = new Configuration.RetainerConfiguration
{ {
RetainerContentId = retainerData.RetainerID,
Name = retainerData.Name, Name = retainerData.Name,
Managed = false, Managed = false,
}; };
@ -54,7 +103,11 @@ partial class AutoRetainerControlPlugin
character.Retainers.Add(retainer); character.Retainers.Add(retainer);
} }
seenRetainers.Add(retainer.Name); if (retainer.Name != retainerData.Name)
{
retainer.Name = retainerData.Name;
save = true;
}
if (retainer.DisplayOrder != retainerData.DisplayOrder) if (retainer.DisplayOrder != retainerData.DisplayOrder)
{ {
@ -107,8 +160,16 @@ partial class AutoRetainerControlPlugin
} }
} }
if (character.Retainers.RemoveAll(x => !seenRetainers.Contains(x.Name)) > 0) if (unknownRetainerIds.Count > 0)
{
foreach (var retainerId in unknownRetainerIds)
{
_pluginLog.Warning($"Removing unknown retainer with contentId {retainerId}");
character.Retainers.RemoveAll(c => c.RetainerContentId == retainerId);
}
save = true; save = true;
}
} }
if (save) if (save)

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using ARControl.External;
using ARControl.GameData; using ARControl.GameData;
using ARControl.Windows; using ARControl.Windows;
using AutoRetainerAPI; using AutoRetainerAPI;
@ -9,13 +10,13 @@ using Dalamud.Game.Command;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads; using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Windowing;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons; using ECommons;
using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET; using ImGuiNET;
using LLib;
namespace ARControl; namespace ARControl;
@ -25,7 +26,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private const int QuickVentureId = 395; private const int QuickVentureId = 395;
private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin)); private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin));
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
@ -35,13 +36,18 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private readonly GameCache _gameCache; private readonly GameCache _gameCache;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly VentureResolver _ventureResolver; private readonly VentureResolver _ventureResolver;
private readonly AllaganToolsIpc _allaganToolsIpc;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly AutoRetainerApi _autoRetainerApi; private readonly AutoRetainerApi _autoRetainerApi;
private readonly AutoRetainerReflection _autoRetainerReflection;
public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, IDataManager dataManager, public AutoRetainerControlPlugin(IDalamudPluginInterface pluginInterface, IDataManager dataManager,
IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider, IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider,
IPluginLog pluginLog) IFramework framework, IPluginLog pluginLog)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_clientState = clientState; _clientState = clientState;
_chatGui = chatGui; _chatGui = chatGui;
@ -53,13 +59,16 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_gameCache = new GameCache(dataManager); _gameCache = new GameCache(dataManager);
_iconCache = new IconCache(textureProvider); _iconCache = new IconCache(textureProvider);
_ventureResolver = new VentureResolver(_gameCache, _pluginLog); _ventureResolver = new VentureResolver(_gameCache, _pluginLog);
DiscardHelperIpc discardHelperIpc = new(_pluginInterface);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog);
_configWindow = _configWindow =
new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache, new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache,
_pluginLog); discardHelperIpc, _allaganToolsIpc, _pluginLog);
_windowSystem.AddWindow(_configWindow); _windowSystem.AddWindow(_configWindow);
ECommonsMain.Init(_pluginInterface, this); ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new(); _autoRetainerApi = new();
_autoRetainerReflection = new AutoRetainerReflection(pluginInterface, framework, pluginLog, _autoRetainerApi);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
@ -72,20 +81,35 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
}); });
if (_autoRetainerApi.Ready) if (_autoRetainerApi.Ready)
Sync(); {
try
{
Sync();
}
catch (Exception e)
{
_pluginLog.Error(e, "Unable to sync characters");
_chatGui.PrintError(
"Unable to synchronize characters with AutoRetainer, plugin might not work properly.");
}
}
} }
private void SendRetainerToVenture(string retainerName) private void SendRetainerToVenture(string retainerName)
{ {
var venture = GetNextVenture(retainerName, false); var venture = GetNextVenture(retainerName, false);
if (venture == QuickVentureId) if (venture.HasValue)
_autoRetainerApi.SetVenture(0);
else if (venture.HasValue)
_autoRetainerApi.SetVenture(venture.Value); _autoRetainerApi.SetVenture(venture.Value);
} }
private unsafe uint? GetNextVenture(string retainerName, bool dryRun) private unsafe uint? GetNextVenture(string retainerName, bool dryRun)
{ {
if (!_autoRetainerReflection.ShouldReassign)
{
_pluginLog.Information("AutoRetainer is configured to not reassign ventures, so we are not checking any venture lists.");
return null;
}
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId); var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null) if (ch == null)
{ {
@ -114,127 +138,124 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginLog.Information("Checking tasks..."); _pluginLog.Information("Checking tasks...");
Sync(); Sync();
var venturesInProgress = CalculateVenturesInProgress(ch);
foreach (var inpr in venturesInProgress)
{
_pluginLog.Verbose($"Venture In Progress: ItemId {inpr.Key} for a total amount of {inpr.Value}");
}
IReadOnlyList<Guid> itemListIds; if (ch.Ventures == 0)
if (ch.Type == Configuration.CharacterType.Standalone) {
itemListIds = ch.ItemListIds; _pluginLog.Warning(
"Could not assign a next venture from venture list, as the character has no ventures left.");
}
else if (ch.Ventures <= _configuration.Misc.VenturesToKeep)
{
_pluginLog.Warning(
$"Could not assign a next venture from venture list, character only has {ch.Ventures} left, configuration says to only send out above {_configuration.Misc.VenturesToKeep} ventures.");
}
else else
{ {
var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId); var venturesInProgress = CalculateVenturesInProgress(ch);
if (group == null) foreach (var inProgress in venturesInProgress)
{ {
_pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}."); _pluginLog.Verbose(
return null; $"Venture In Progress: ItemId {inProgress.Key} for a total amount of {inProgress.Value}");
} }
itemListIds = group.ItemListIds; IReadOnlyList<Guid> itemListIds;
} if (ch.Type == Configuration.CharacterType.Standalone)
itemListIds = ch.ItemListIds;
var itemLists = itemListIds.Where(listId => listId != Guid.Empty)
.Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
.Where(list => list != null)
.Cast<Configuration.ItemList>()
.ToList();
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var list in itemLists)
{
_pluginLog.Information($"Checking ventures in list '{list.Name}'");
IReadOnlyList<StockedItem> itemsOnList;
if (list.Type == Configuration.ListType.CollectOneTime)
{
itemsOnList = list.Items
.Select(x => new StockedItem
{
QueuedItem = x,
InventoryCount = 0,
})
.Where(x => x.RequestedCount > 0)
.ToList()
.AsReadOnly();
}
else else
{ {
itemsOnList = list.Items var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId);
.Select(x => new StockedItem if (group == null)
{ {
QueuedItem = x, _pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}.");
InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) + return null;
(venturesInProgress.TryGetValue(x.ItemId, out int inProgress) }
? inProgress
: 0),
})
.Where(x => x.InventoryCount <= x.RequestedCount)
.ToList()
.AsReadOnly();
// collect items with the least current inventory first itemListIds = group.ItemListIds;
if (list.Priority == Configuration.ListPriority.Balanced)
itemsOnList = itemsOnList.OrderBy(x => x.InventoryCount).ToList().AsReadOnly();
} }
_pluginLog.Debug($"Found {itemsOnList.Count} to-do items on current list"); var itemLists = itemListIds.Where(listId => listId != Guid.Empty)
if (itemsOnList.Count == 0) .Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
continue; .Where(list => list != null)
.Cast<Configuration.ItemList>()
foreach (var itemOnList in itemsOnList) .ToList();
InventoryManager* inventoryManager = InventoryManager.Instance();
foreach (var list in itemLists)
{ {
_pluginLog.Debug($"Checking venture info for itemId {itemOnList.ItemId}"); _pluginLog.Information($"Checking ventures in list '{list.Name}'");
IReadOnlyList<StockedItem> itemsOnList;
var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, itemOnList.ItemId); if (list.Type == Configuration.ListType.CollectOneTime)
if (venture == null)
{ {
venture = _gameCache.Ventures.FirstOrDefault(x => x.ItemId == itemOnList.ItemId); itemsOnList = list.Items
_pluginLog.Debug($"Retainer doesn't know how to gather itemId {itemOnList.ItemId} ({venture?.Name})"); .Select(x => new StockedItem
} {
else if (reward == null) QueuedItem = x,
{ InventoryCount = 0,
_pluginLog.Debug($"Retainer can't complete venture '{venture.Name}'"); })
.Where(x => x.RequestedCount > 0)
.ToList()
.AsReadOnly();
} }
else else
{ {
_chatGui.Print( itemsOnList = list.Items
new SeString(new UIForegroundPayload(579)) .Select(x => new StockedItem
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" Sending retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(" to collect ")
.Append(new UIForegroundPayload(1))
.Append($"{reward.Quantity}x ")
.Append(new ItemPayload(venture.ItemId))
.Append(venture.Name)
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" for ")
.Append(new UIForegroundPayload(1))
.Append($"{list.Name} {list.GetIcon()}")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
if (!dryRun)
{
retainer.HasVenture = true;
retainer.LastVenture = venture.RowId;
if (list.Type == Configuration.ListType.CollectOneTime)
{ {
itemOnList.RequestedCount = QueuedItem = x,
Math.Max(0, itemOnList.RequestedCount - reward.Quantity); InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) +
venturesInProgress.GetValueOrDefault(x.ItemId, 0) +
(list.CheckRetainerInventory
? (int)_allaganToolsIpc.GetRetainerItemCount(x.ItemId)
: 0),
})
.Where(x => x.InventoryCount < x.RequestedCount)
.ToList()
.AsReadOnly();
// collect items with the least current inventory first
if (list.Priority == Configuration.ListPriority.Balanced)
itemsOnList = itemsOnList.OrderBy(x => x.InventoryCount).ToList().AsReadOnly();
}
_pluginLog.Debug($"Found {itemsOnList.Count} to-do items on current list");
if (itemsOnList.Count == 0)
continue;
foreach (var itemOnList in itemsOnList)
{
_pluginLog.Debug($"Checking venture info for itemId {itemOnList.ItemId}");
var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, itemOnList.ItemId);
if (venture == null)
{
venture = _gameCache.Ventures.FirstOrDefault(x => x.ItemId == itemOnList.ItemId);
_pluginLog.Debug(
$"Retainer doesn't know how to gather itemId {itemOnList.ItemId} ({venture?.Name})");
}
else if (reward == null)
{
_pluginLog.Debug($"Retainer can't complete venture '{venture.Name}'");
}
else
{
if (_configuration.ConfigUiOptions.ShowAssignmentChatMessages || dryRun)
PrintNextVentureMessage(retainerName, venture, reward, list);
if (!dryRun)
{
retainer.HasVenture = true;
retainer.LastVenture = venture.RowId;
if (list.Type == Configuration.ListType.CollectOneTime)
{
itemOnList.RequestedCount =
Math.Max(0, itemOnList.RequestedCount - reward.Quantity);
}
_pluginInterface.SavePluginConfig(_configuration);
} }
_pluginInterface.SavePluginConfig(_configuration); return venture.RowId;
} }
return venture.RowId;
} }
} }
} }
@ -242,21 +263,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
// fallback: managed but no venture found/ // fallback: managed but no venture found/
if (retainer.LastVenture != QuickVentureId) if (retainer.LastVenture != QuickVentureId)
{ {
_chatGui.Print( PrintEndOfListMessage(retainerName, retainer);
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" No tasks left for retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(", sending to ")
.Append(new UIForegroundPayload(1))
.Append("Quick Venture")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QV");
if (!dryRun) if (!dryRun)
{ {
retainer.HasVenture = true; retainer.HasVenture = true;
@ -264,6 +271,13 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
} }
// Unsure if this (eventually) will do venture plans you've configured in AutoRetainer, but by default
// (with Assign + Reassign) as config options, returning `0` here as suggested in
// https://discord.com/channels/1001823907193552978/1001825038598676530/1161295221447983226
// will just repeat the last venture.
//
// That makes sense, of course, but it's also not really the desired behaviour for when you're at the end
// of a list.
return QuickVentureId; return QuickVentureId;
} }
else else
@ -273,6 +287,50 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
} }
} }
private void PrintNextVentureMessage(string retainerName, Venture venture, VentureReward reward, Configuration.ItemList list)
{
_chatGui.Print(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" Sending retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(" to collect ")
.Append(new UIForegroundPayload(1))
.Append($"{reward.Quantity}x ")
.Append(new ItemPayload(venture.ItemId))
.Append(venture.Name)
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" for ")
.Append(new UIForegroundPayload(1))
.Append($"{list.Name} {list.GetIcon()}")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
}
private void PrintEndOfListMessage(string retainerName, Configuration.RetainerConfiguration retainer)
{
_chatGui.Print(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" No tasks left for retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(", sending to ")
.Append(new UIForegroundPayload(1))
.Append("Quick Venture")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QV");
}
/// <remarks> /// <remarks>
/// This treats the retainer who is currently doing the venture as 'in-progress', since I believe the /// This treats the retainer who is currently doing the venture as 'in-progress', since I believe the
/// relevant event is fired BEFORE the venture rewards are collected. /// relevant event is fired BEFORE the venture rewards are collected.
@ -341,7 +399,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{ {
if (arguments == "sync") if (arguments == "sync")
Sync(); Sync();
else if (arguments.StartsWith("dnv")) else if (arguments.StartsWith("dnv", StringComparison.Ordinal))
{ {
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId); var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null || ch.Type == Configuration.CharacterType.NotManaged || ch.Retainers.Count == 0) if (ch == null || ch.Type == Configuration.CharacterType.NotManaged || ch.Retainers.Count == 0)
@ -355,7 +413,10 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
if (s.Length > 1) if (s.Length > 1)
retainerName = ch.Retainers.SingleOrDefault(x => x.Name.EqualsIgnoreCase(s[1]))?.Name; retainerName = ch.Retainers.SingleOrDefault(x => x.Name.EqualsIgnoreCase(s[1]))?.Name;
else else
retainerName = ch.Retainers.MinBy(x => x.DisplayOrder)?.Name; retainerName = ch.Retainers
.OrderBy(x => x.DisplayOrder)
.ThenBy(x => x.RetainerContentId)
.FirstOrDefault()?.Name;
if (retainerName == null) if (retainerName == null)
{ {
@ -389,6 +450,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_iconCache.Dispose(); _iconCache.Dispose();
_autoRetainerReflection.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();
ECommonsMain.Dispose(); ECommonsMain.Dispose();
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Newtonsoft.Json;
namespace ARControl; namespace ARControl;
@ -12,6 +13,8 @@ internal sealed class Configuration : IPluginConfiguration
public List<CharacterConfiguration> Characters { get; set; } = new(); public List<CharacterConfiguration> Characters { get; set; } = new();
public List<ItemList> ItemLists { get; set; } = new(); public List<ItemList> ItemLists { get; set; } = new();
public List<CharacterGroup> CharacterGroups { get; set; } = new(); public List<CharacterGroup> CharacterGroups { get; set; } = new();
public MiscConfiguration Misc { get; set; } = new();
public ConfigWindowUiOptions ConfigUiOptions { get; set; } = new();
public sealed class ItemList public sealed class ItemList
{ {
@ -19,10 +22,14 @@ internal sealed class Configuration : IPluginConfiguration
public required string Name { get; set; } public required string Name { get; set; }
public required ListType Type { get; set; } = ListType.CollectOneTime; public required ListType Type { get; set; } = ListType.CollectOneTime;
public required ListPriority Priority { get; set; } = ListPriority.InOrder; public required ListPriority Priority { get; set; } = ListPriority.InOrder;
public bool CheckRetainerInventory { get; set; }
public List<QueuedItem> Items { get; set; } = new(); public List<QueuedItem> Items { get; set; } = new();
public string GetIcon() public string GetIcon()
{ {
if (Id == Guid.Empty)
return string.Empty;
return Type switch return Type switch
{ {
ListType.CollectOneTime => SeIconChar.BoxedNumber1.ToIconString(), ListType.CollectOneTime => SeIconChar.BoxedNumber1.ToIconString(),
@ -47,6 +54,9 @@ internal sealed class Configuration : IPluginConfiguration
public sealed class QueuedItem public sealed class QueuedItem
{ {
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
public required uint ItemId { get; set; } public required uint ItemId { get; set; }
public required int RemainingQuantity { get; set; } public required int RemainingQuantity { get; set; }
} }
@ -64,12 +74,14 @@ internal sealed class Configuration : IPluginConfiguration
public required string CharacterName { get; set; } public required string CharacterName { get; set; }
public required string WorldName { get; set; } public required string WorldName { get; set; }
public uint Ventures { get; set; }
public CharacterType Type { get; set; } = CharacterType.NotManaged; public CharacterType Type { get; set; } = CharacterType.NotManaged;
public Guid CharacterGroupId { get; set; } public Guid CharacterGroupId { get; set; }
public List<Guid> ItemListIds { get; set; } = new(); public List<Guid> ItemListIds { get; set; } = new();
public List<RetainerConfiguration> Retainers { get; set; } = new(); public List<RetainerConfiguration> Retainers { get; set; } = new();
public HashSet<uint> GatheredItems { get; set; } = new(); public HashSet<uint> GatheredItems { get; set; } = new();
public HashSet<uint> UnlockedFolkloreBooks { get; set; } = new();
public override string ToString() => $"{CharacterName} @ {WorldName}"; public override string ToString() => $"{CharacterName} @ {WorldName}";
} }
@ -91,6 +103,7 @@ internal sealed class Configuration : IPluginConfiguration
public sealed class RetainerConfiguration public sealed class RetainerConfiguration
{ {
public ulong RetainerContentId { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public required bool Managed { get; set; } public required bool Managed { get; set; }
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }
@ -102,4 +115,17 @@ internal sealed class Configuration : IPluginConfiguration
public int Gathering { get; set; } public int Gathering { get; set; }
public int Perception { get; set; } public int Perception { get; set; }
} }
public sealed class MiscConfiguration
{
public int VenturesToKeep { get; set; }
}
public sealed class ConfigWindowUiOptions
{
public bool ShowVentureListContents { get; set; } = true;
public bool CheckGatheredItemsPerCharacter { get; set; }
public bool OnlyShowMissingGatheredItems { get; set; }
public bool ShowAssignmentChatMessages { get; set; } = true;
}
} }

77
ARControl/External/AllaganToolsIpc.cs vendored Normal file
View File

@ -0,0 +1,77 @@
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
namespace ARControl.External;
internal sealed class AllaganToolsIpc
{
private readonly IPluginLog _pluginLog;
private static readonly uint[] RetainerInventoryTypes = new[]
{
InventoryType.RetainerPage1,
InventoryType.RetainerPage2,
InventoryType.RetainerPage3,
InventoryType.RetainerPage4,
InventoryType.RetainerPage5,
InventoryType.RetainerPage6,
InventoryType.RetainerPage7,
InventoryType.RetainerCrystals,
}
.Select(x => (uint)x).ToArray();
private readonly ICallGateSubscriber<ulong,HashSet<ulong[]>> _getClownItems;
private readonly ICallGateSubscriber<uint, bool, uint[], uint> _itemCountOwned;
public AllaganToolsIpc(IDalamudPluginInterface pluginInterface, IPluginLog pluginLog)
{
_pluginLog = pluginLog;
_getClownItems = pluginInterface.GetIpcSubscriber<ulong, HashSet<ulong[]>>("AllaganTools.GetCharacterItems");
_itemCountOwned = pluginInterface.GetIpcSubscriber<uint, bool, uint[], uint>("AllaganTools.ItemCountOwned");
}
public List<(uint ItemId, uint Quantity)> GetCharacterItems(ulong contentId)
{
try
{
HashSet<ulong[]> items = _getClownItems.InvokeFunc(contentId);
_pluginLog.Information($"CID: {contentId}, Items: {items.Count}");
return items.Select(x => (ItemId: (uint)x[2], Quantity: (uint)x[3]))
.GroupBy(x => x.ItemId)
.Select(x => (x.Key, (uint)x.Sum(y => y.Quantity)))
.ToList();
}
catch (TargetInvocationException e)
{
_pluginLog.Information(e, $"Unable to retrieve items for character {contentId}");
return [];
}
catch (IpcError)
{
_pluginLog.Warning("Could not query allagantools for character items");
return [];
}
}
public uint GetRetainerItemCount(uint itemId)
{
try
{
uint itemCount = _itemCountOwned.InvokeFunc(itemId, true, RetainerInventoryTypes);
_pluginLog.Verbose($"Found {itemCount} items in retainer inventories for itemId {itemId}");
return itemCount;
}
catch (IpcError)
{
_pluginLog.Warning("Could not query allagantools for retainer inventory counts");
return 0;
}
}
}

View File

@ -0,0 +1,68 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
using AutoRetainerAPI;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using LLib;
namespace ARControl.External
{
internal sealed class AutoRetainerReflection : IDisposable
{
private readonly IPluginLog _pluginLog;
private readonly AutoRetainerApi _autoRetainerApi;
private readonly DalamudReflector _reflector;
public AutoRetainerReflection(IDalamudPluginInterface pluginInterface, IFramework framework,
IPluginLog pluginLog, AutoRetainerApi autoRetainerApi)
{
_pluginLog = pluginLog;
_autoRetainerApi = autoRetainerApi;
_reflector = new DalamudReflector(pluginInterface, framework, pluginLog);
}
[SuppressMessage("Performance", "CA2000", Justification = "Should not dispose other plugins")]
public bool ShouldReassign
{
get
{
try
{
if (_autoRetainerApi.Ready &&
_reflector.TryGetDalamudPlugin("AutoRetainer", out var autoRetainer, false, true))
{
var config =
autoRetainer.GetType().GetProperty("C", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null);
if (config == null)
{
_pluginLog.Warning("Could not retrieve AR config");
return true;
}
bool dontReassign = (bool)config.GetType()
.GetField("_dontReassign", BindingFlags.Instance | BindingFlags.Public)!
.GetValue(config)!;
_pluginLog.Verbose($"DontReassign is set to {dontReassign}");
return !dontReassign;
}
_pluginLog.Warning("Could not check if reassign is enabled, AutoRetainer not loaded");
return true;
}
catch (Exception e)
{
_pluginLog.Warning(e, "Unable to check if reassign is enabled");
return true;
}
}
}
public void Dispose()
{
_reflector.Dispose();
}
}
}

30
ARControl/External/DiscardHelperIpc.cs vendored Normal file
View File

@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Collections.Immutable;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
namespace ARControl.External;
internal sealed class DiscardHelperIpc
{
private readonly ICallGateSubscriber<IReadOnlySet<uint>> _itemsToDiscard;
public DiscardHelperIpc(IDalamudPluginInterface pluginInterface)
{
_itemsToDiscard = pluginInterface.GetIpcSubscriber<IReadOnlySet<uint>>("ARDiscard.GetItemsToDiscard");
}
public IReadOnlySet<uint> GetItemsToDiscard()
{
try
{
return _itemsToDiscard.InvokeFunc();
}
catch (IpcError)
{
// ignore
return ImmutableHashSet<uint>.Empty;
}
}
}

View File

@ -0,0 +1,18 @@
using System.Collections.Generic;
using FFXIVClientStructs.FFXIV.Client.Game.UI;
namespace ARControl.GameData;
internal sealed class FolkloreBook
{
public required uint ItemId { get; init; }
public required string Name { get; init; }
public required IReadOnlyList<ushort> GatheringSubCategories { get; init; }
public List<uint> GatheringItemIds { get; } = [];
public ushort TomeId { get; set; }
public unsafe bool IsUnlocked()
{
return PlayerState.Instance()->IsFolkloreBookUnlocked(TomeId);
}
}

View File

@ -22,9 +22,74 @@ internal sealed class GameCache
.OrderBy(x => x.Name) .OrderBy(x => x.Name)
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
FolkloreBooks = dataManager.GetExcelSheet<GatheringSubCategory>()!
.Where(x => x.RowId > 0)
.Where(x => x.Item.Row != 0)
.Select(x => new
{
x.RowId,
ItemId = x.Item.Row,
ItemName = x.Item.Value!.Name.ToString()
})
.GroupBy(x => (x.ItemId, x.ItemName))
.Select(x =>
new FolkloreBook
{
ItemId = x.Key.ItemId,
Name = x.Key.ItemName,
GatheringSubCategories = x.Select(y => (ushort)y.RowId).ToList(),
})
.ToDictionary(x => x.ItemId, x => x);
var gatheringNodes = dataManager.GetExcelSheet<GatheringPointBase>()!
.Where(x => x.RowId > 0 && x.GatheringType.Row <= 3)
.Select(x =>
new
{
GatheringPointBaseId = x.RowId,
GatheringPoint =
dataManager.GetExcelSheet<GatheringPoint>()!.FirstOrDefault(y =>
y.GatheringPointBase.Row == x.RowId),
Items = x.Item.Where(y => y > 0).ToList()
})
.Where(x => x.GatheringPoint != null)
.Select(x =>
new
{
x.GatheringPointBaseId,
CategoryId = (ushort)x.GatheringPoint!.GatheringSubCategory.Row,
x.Items,
})
.ToList();
var itemsWithoutTomes = gatheringNodes
.Where(x => !FolkloreBooks.Values.Any(y => y.GatheringSubCategories.Contains(x.CategoryId)))
.SelectMany(x => x.Items)
.ToList();
var itemsWithTomes = gatheringNodes
.SelectMany(x => x.Items
.Where(y => !itemsWithoutTomes.Contains(y))
.Select(
y =>
new
{
x.CategoryId,
ItemId = (uint)y
}))
.GroupBy(x => x.CategoryId)
.ToDictionary(x => x.Key, x => x.Select(y => y.ItemId).ToList());
foreach (var book in FolkloreBooks.Values)
{
book.TomeId = dataManager.GetExcelSheet<Item>()!.GetRow(book.ItemId)!.ItemAction.Value!.Data[0];
foreach (var category in book.GatheringSubCategories)
{
if (itemsWithTomes.TryGetValue(category, out var itemsInCategory))
book.GatheringItemIds.AddRange(itemsInCategory);
}
}
} }
public IReadOnlyDictionary<uint, string> Jobs { get; } public IReadOnlyDictionary<uint, string> Jobs { get; }
public IReadOnlyList<Venture> Ventures { get; } public IReadOnlyList<Venture> Ventures { get; }
public IReadOnlyList<ItemToGather> ItemsToGather { get; } public IReadOnlyList<ItemToGather> ItemsToGather { get; }
public Dictionary<uint, FolkloreBook> FolkloreBooks { get; }
} }

View File

@ -1,53 +0,0 @@
using System;
using System.Collections.Generic;
using Dalamud.Interface.Internal;
using Dalamud.Plugin.Services;
namespace ARControl;
internal sealed class IconCache : IDisposable
{
private readonly ITextureProvider _textureProvider;
private readonly Dictionary<uint, TextureContainer> _textureWraps = new();
public IconCache(ITextureProvider textureProvider)
{
_textureProvider = textureProvider;
}
public IDalamudTextureWrap? GetIcon(uint iconId)
{
if (_textureWraps.TryGetValue(iconId, out TextureContainer? container))
return container.Texture;
var iconTex = _textureProvider.GetIcon(iconId);
if (iconTex != null)
{
if (iconTex.ImGuiHandle != nint.Zero)
{
_textureWraps[iconId] = new TextureContainer { Texture = iconTex };
return iconTex;
}
iconTex.Dispose();
}
_textureWraps[iconId] = new TextureContainer { Texture = null };
return null;
}
public void Dispose()
{
foreach (TextureContainer container in _textureWraps.Values)
container.Dispose();
_textureWraps.Clear();
}
private sealed class TextureContainer : IDisposable
{
public required IDalamudTextureWrap? Texture { get; init; }
public void Dispose() => Texture?.Dispose();
}
}

View File

@ -0,0 +1,207 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using ECommons;
using ImGuiNET;
namespace ARControl.Windows.Config;
internal sealed class CharacterGroupTab : ITab
{
private readonly ConfigWindow _configWindow;
private readonly Configuration _configuration;
private readonly Dictionary<Guid, TemporaryConfig> _currentEditPopups = new();
private TemporaryConfig _newGroup = new() { Name = string.Empty };
public CharacterGroupTab(ConfigWindow configWindow, Configuration configuration)
{
_configWindow = configWindow;
_configuration = configuration;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Groups###TabGroups");
if (!tab)
return;
Configuration.CharacterGroup? groupToDelete = null;
foreach (var group in _configuration.CharacterGroups)
{
ImGui.PushID($"##Group{group.Id}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
{
_currentEditPopups[group.Id] = new TemporaryConfig
{
Name = group.Name,
};
ImGui.OpenPopup($"##EditGroup{group.Id}");
}
DrawCharacterGroupEditorPopup(group, out var assignedCharacters, ref groupToDelete);
ImGui.SameLine();
DrawCharacterGroup(group, assignedCharacters);
ImGui.PopID();
}
if (groupToDelete != null)
{
_configuration.CharacterGroups.Remove(groupToDelete);
_configWindow.ShouldSave();
}
ImGui.Separator();
DrawNewCharacterGroup();
}
private void DrawCharacterGroupEditorPopup(Configuration.CharacterGroup group,
out List<Configuration.CharacterConfiguration> assignedCharacters,
ref Configuration.CharacterGroup? groupToDelete)
{
assignedCharacters = _configuration.Characters
.Where(x => x.Type == Configuration.CharacterType.PartOfCharacterGroup &&
x.CharacterGroupId == group.Id)
.OrderBy(x => x.WorldName)
.ThenBy(x => x.LocalContentId)
.ToList();
if (_currentEditPopups.TryGetValue(group.Id, out TemporaryConfig? temporaryConfig) &&
ImGui.BeginPopup($"##EditGroup{group.Id}"))
{
(bool save, bool canSave) = DrawGroupEditor(temporaryConfig, group);
ImGui.BeginDisabled(!canSave || group.Name == temporaryConfig.Name);
save |= ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save");
ImGui.EndDisabled();
if (save && canSave)
{
group.Name = temporaryConfig.Name;
ImGui.CloseCurrentPopup();
_configWindow.ShouldSave();
}
else
{
ImGui.SameLine();
ImGui.BeginDisabled(assignedCharacters.Count > 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Delete"))
{
groupToDelete = group;
ImGui.CloseCurrentPopup();
}
ImGui.EndDisabled();
if (assignedCharacters.Count > 0 && ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.BeginTooltip();
ImGui.Text(
$"Remove the {assignedCharacters.Count} character(s) from this group before deleting it.");
foreach (var character in assignedCharacters)
ImGui.BulletText($"{character.CharacterName} @ {character.WorldName}");
ImGui.EndTooltip();
}
}
ImGui.EndPopup();
}
}
private void DrawCharacterGroup(Configuration.CharacterGroup group,
List<Configuration.CharacterConfiguration> assignedCharacters)
{
string countLabel = assignedCharacters.Count == 0 ? "no characters"
: assignedCharacters.Count == 1 ? "1 character"
: $"{assignedCharacters.Count} characters";
if (ImGui.CollapsingHeader($"{group.Name} ({countLabel})"))
{
ImGui.Indent(_configWindow.MainIndentSize);
using (var tabBar = ImRaii.TabBar("GroupOptions"))
{
if (tabBar)
{
using (var ventureListTab = ImRaii.TabItem("Venture Lists"))
{
if (ventureListTab)
_configWindow.DrawVentureListSelection(group.Id.ToString(), group.ItemListIds);
}
using (var charactersTab = ImRaii.TabItem("Characters"))
{
if (charactersTab)
{
ImGui.Text("Characters in this group:");
ImGui.Indent(_configWindow.MainIndentSize);
foreach (var character in assignedCharacters.OrderBy(x => x.WorldName)
.ThenBy(x => x.LocalContentId))
ImGui.TextUnformatted($"{character.CharacterName} @ {character.WorldName}");
ImGui.Unindent(_configWindow.MainIndentSize);
}
}
}
}
ImGui.Unindent(_configWindow.MainIndentSize);
}
}
private void DrawNewCharacterGroup()
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Group"))
ImGui.OpenPopup("##AddGroup");
if (ImGui.BeginPopup("##AddGroup"))
{
(bool save, bool canSave) = DrawGroupEditor(_newGroup, null);
ImGui.BeginDisabled(!canSave);
save |= ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save");
ImGui.EndDisabled();
if (save && canSave)
{
_configuration.CharacterGroups.Add(new Configuration.CharacterGroup
{
Id = Guid.NewGuid(),
Name = _newGroup.Name,
ItemListIds = new(),
});
_newGroup = new() { Name = string.Empty };
ImGui.CloseCurrentPopup();
_configWindow.ShouldSave();
}
ImGui.EndPopup();
}
}
private (bool Save, bool CanSave) DrawGroupEditor(TemporaryConfig group,
Configuration.CharacterGroup? existingGroup)
{
string name = group.Name;
bool save = ImGui.InputTextWithHint("", "Group Name...", ref name, 64, ImGuiInputTextFlags.EnterReturnsTrue);
bool canSave = IsValidGroupName(name, existingGroup);
group.Name = name;
return (save, canSave);
}
private bool IsValidGroupName(string name, Configuration.CharacterGroup? existingGroup)
{
return name.Length >= 2 &&
!name.Contains('%', StringComparison.Ordinal) &&
!_configuration.CharacterGroups.Any(x => x != existingGroup && name.EqualsIgnoreCase(x.Name));
}
private sealed class TemporaryConfig
{
public required string Name { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace ARControl.Windows.Config;
internal interface ITab
{
public void Draw();
}

View File

@ -0,0 +1,295 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Linq;
using ARControl.External;
using ARControl.GameData;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using ImGuiNET;
namespace ARControl.Windows.Config;
internal sealed class InventoryTab : ITab
{
private readonly Configuration _configuration;
private readonly AllaganToolsIpc _allaganToolsIpc;
private readonly GameCache _gameCache;
private readonly IPluginLog _pluginLog;
private List<TreeNode>? _listAsTrees;
private DateTime? _lastUpdate;
public InventoryTab(Configuration configuration, AllaganToolsIpc allaganToolsIpc, GameCache gameCache,
IPluginLog pluginLog)
{
_configuration = configuration;
_allaganToolsIpc = allaganToolsIpc;
_gameCache = gameCache;
_pluginLog = pluginLog;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Inventory###TabInventory");
if (!tab)
{
_listAsTrees = null;
_lastUpdate = null;
return;
}
if (_listAsTrees == null)
RefreshInventory();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Redo, "Refresh"))
RefreshInventory();
if (_lastUpdate != null)
{
string text = $"Last Update: {_lastUpdate:t}";
ImGui.SameLine();
ImGui.SameLine(ImGui.GetContentRegionAvail().X - ImGui.CalcTextSize(text).X + ImGui.GetCursorPosX());
ImGui.AlignTextToFramePadding();
ImGui.TextColored(ImGuiColors.DalamudGrey, text);
}
ImGui.Separator();
if (_listAsTrees == null || _listAsTrees.Count == 0)
{
ImGui.Text("No items in inventory. Do you have AllaganTools installed?");
return;
}
foreach (var list in _configuration.ItemLists)
{
using var id = ImRaii.PushId($"List{list.Id}");
if (ImGui.CollapsingHeader($"{list.Name} {list.GetIcon()}"))
{
using var indent = ImRaii.PushIndent();
var rootNode = _listAsTrees.FirstOrDefault(x => x.Id == list.Id.ToString());
if (rootNode == null || rootNode.Children.Count == 0)
{
ImGui.Text("This list is empty.");
continue;
}
using var table = ImRaii.Table($"InventoryTable{list.Id}", 2, ImGuiTableFlags.NoSavedSettings);
if (!table)
continue;
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.NoHide);
ImGui.TableSetupColumn("", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
foreach (var child in rootNode.Children)
child.Draw();
}
}
}
private void RefreshInventory()
{
try
{
List<CharacterInventory> inventories = new();
foreach (Configuration.CharacterConfiguration character in _configuration.Characters)
{
List<Guid> itemListIds = new();
if (character.Type == Configuration.CharacterType.Standalone)
{
itemListIds = character.ItemListIds;
}
else if (character.Type == Configuration.CharacterType.PartOfCharacterGroup)
{
var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == character.CharacterGroupId);
if (group != null)
itemListIds = group.ItemListIds;
}
var inventory = new CharacterInventory(character, itemListIds);
var itemIdsOnLists = itemListIds.Where(listId => listId != Guid.Empty)
.Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
.Where(list => list != null)
.SelectMany(list => list!.Items.Select(x => x.ItemId))
.Distinct()
.ToHashSet();
UpdateOwnedItems(character.LocalContentId, inventory.Items, itemIdsOnLists);
foreach (var retainer in inventory.Retainers)
UpdateOwnedItems(retainer.Configuration.RetainerContentId, retainer.Items, itemIdsOnLists);
inventories.Add(inventory);
}
List<TreeNode> listAsTrees = [];
if (inventories.Count > 0)
{
foreach (var list in _configuration.ItemLists)
{
TreeNode rootNode = new TreeNode(list.Id.ToString(), string.Empty, -1);
listAsTrees.Add(rootNode);
var relevantCharacters = inventories.Where(x => x.ItemListIds.Contains(list.Id)).ToList();
foreach (var item in list.Items)
{
var venture = _gameCache.Ventures.FirstOrDefault(x => x.ItemId == item.ItemId);
var total = relevantCharacters.Sum(x => x.CountItems(item.ItemId, list.CheckRetainerInventory));
TreeNode itemNode = rootNode.AddChild(item.InternalId.ToString(), venture?.Name ?? string.Empty,
total);
foreach (var character in relevantCharacters)
{
string characterName =
$"{character.Configuration.CharacterName} @ {character.Configuration.WorldName}";
long? stockQuantity = list.Type == Configuration.ListType.KeepStocked
? item.RemainingQuantity
: null;
uint characterCount = character.CountItems(item.ItemId, list.CheckRetainerInventory);
if (characterCount == 0)
continue;
var characterNode = itemNode.AddChild(
character.Configuration.LocalContentId.ToString(CultureInfo.InvariantCulture),
characterName, characterCount, stockQuantity);
if (list.CheckRetainerInventory)
{
characterNode.AddChild("Self", "In Inventory",
character.CountItems(item.ItemId, false));
foreach (var retainer in character.Retainers)
{
uint retainerCount = retainer.CountItems(item.ItemId);
if (retainerCount == 0)
continue;
characterNode.AddChild(
retainer.Configuration.RetainerContentId.ToString(CultureInfo.InvariantCulture),
retainer.Configuration.Name, retainerCount);
}
}
}
}
}
}
_listAsTrees = listAsTrees;
_lastUpdate = DateTime.Now;
}
catch (Exception e)
{
_listAsTrees = [];
_pluginLog.Error(e, "Failed to load inventories via AllaganTools");
}
}
private void UpdateOwnedItems(ulong localContentId, List<Item> items, HashSet<uint> itemIdsOnLists)
{
var ownedItems = _allaganToolsIpc.GetCharacterItems(localContentId);
foreach (var ownedItem in ownedItems)
{
if (!itemIdsOnLists.Contains(ownedItem.ItemId))
continue;
items.Add(new Item(ownedItem.ItemId, ownedItem.Quantity));
}
}
private sealed class CharacterInventory
{
public CharacterInventory(Configuration.CharacterConfiguration configuration, List<Guid> itemListIds)
{
Configuration = configuration;
ItemListIds = itemListIds;
Retainers = configuration.Retainers.Where(x => x is { Job: > 0, Managed: true })
.OrderBy(x => x.DisplayOrder)
.ThenBy(x => x.RetainerContentId)
.Select(x => new RetainerInventory(x))
.ToList();
}
public Configuration.CharacterConfiguration Configuration { get; }
public List<Guid> ItemListIds { get; }
public List<RetainerInventory> Retainers { get; }
public List<Item> Items { get; } = [];
public uint CountItems(uint itemId, bool checkRetainerInventory)
{
uint sum = (uint)Items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity);
if (checkRetainerInventory)
sum += (uint)Retainers.Sum(x => x.CountItems(itemId));
return sum;
}
}
private sealed class RetainerInventory(Configuration.RetainerConfiguration configuration)
{
public Configuration.RetainerConfiguration Configuration { get; } = configuration;
public List<Item> Items { get; } = [];
public uint CountItems(uint itemId) => (uint)Items.Where(x => x.ItemId == itemId).Sum(x => x.Quantity);
}
private sealed record Item(uint ItemId, uint Quantity);
private sealed record TreeNode(string Id, string Label, long Quantity, long? StockQuantity = null)
{
public List<TreeNode> Children { get; } = [];
public TreeNode AddChild(string id, string label, long quantity, long? stockQuantity = null)
{
TreeNode child = new TreeNode(id, label, quantity, stockQuantity);
Children.Add(child);
return child;
}
public void Draw()
{
ImGui.TableNextRow();
ImGui.TableNextColumn();
if (Children.Count > 0)
{
bool open = ImGui.TreeNodeEx(Label, ImGuiTreeNodeFlags.SpanFullWidth);
ImGui.TableNextColumn();
DrawCount();
if (open)
{
foreach (var child in Children)
child.Draw();
ImGui.TreePop();
}
}
else
{
ImGui.TreeNodeEx(Label,
ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
ImGui.TableNextColumn();
DrawCount();
}
}
private void DrawCount()
{
if (StockQuantity != null)
{
using var color = ImRaii.PushColor(ImGuiCol.Text, Quantity >= StockQuantity.Value
? ImGuiColors.HealerGreen
: ImGuiColors.DalamudRed);
ImGui.TextUnformatted(string.Create(CultureInfo.CurrentCulture,
$"{Quantity:N0} / {StockQuantity.Value:N0}"));
}
else
ImGui.TextUnformatted(Quantity.ToString("N0", CultureInfo.CurrentCulture));
}
}
}

View File

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using ARControl.GameData;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using ImGuiNET;
namespace ARControl.Windows.Config;
internal sealed class LockedItemsTab : ITab
{
private static readonly Vector4 ColorGreen = ImGuiColors.HealerGreen;
private static readonly Vector4 ColorRed = ImGuiColors.DalamudRed;
private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey;
private static readonly string CurrentCharPrefix = FontAwesomeIcon.Male.ToIconString();
private readonly ConfigWindow _configWindow;
private readonly Configuration _configuration;
private readonly IClientState _clientState;
private readonly ICommandManager _commandManager;
private readonly GameCache _gameCache;
public LockedItemsTab(ConfigWindow configWindow, Configuration configuration, IClientState clientState,
ICommandManager commandManager, GameCache gameCache)
{
_configWindow = configWindow;
_configuration = configuration;
_clientState = clientState;
_commandManager = commandManager;
_gameCache = gameCache;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Locked Items###TabLockedItems");
if (!tab)
return;
bool checkPerCharacter = _configuration.ConfigUiOptions.CheckGatheredItemsPerCharacter;
if (ImGui.Checkbox("Group by character", ref checkPerCharacter))
{
_configuration.ConfigUiOptions.CheckGatheredItemsPerCharacter = checkPerCharacter;
_configWindow.ShouldSave();
}
bool onlyShowMissing = _configuration.ConfigUiOptions.OnlyShowMissingGatheredItems;
if (ImGui.Checkbox("Only show missing items", ref onlyShowMissing))
{
_configuration.ConfigUiOptions.OnlyShowMissingGatheredItems = onlyShowMissing;
_configWindow.ShouldSave();
}
ImGui.Separator();
var itemsToCheck =
_configuration.ItemLists
.SelectMany(x => x.Items)
.Select(x => x.ItemId)
.Distinct()
.Select(itemId => new
{
GatheredItem = _gameCache.ItemsToGather.SingleOrDefault(x => x.ItemId == itemId),
Ventures = _gameCache.Ventures.Where(x => x.ItemId == itemId).ToList()
})
.Where(x => x.GatheredItem != null && x.Ventures.Count > 0)
.Select(x => new CheckedItem
{
GatheredItem = x.GatheredItem!,
Ventures = x.Ventures,
ItemId = x.Ventures.First().ItemId,
})
.ToList();
var charactersToCheck = _configuration.Characters
.Where(x => x.Type != Configuration.CharacterType.NotManaged)
.OrderBy(x => x.WorldName)
.ThenBy(x => x.LocalContentId)
.Select(x => new CheckedCharacter(_configuration, x, itemsToCheck))
.ToList();
if (checkPerCharacter)
{
foreach (var ch in charactersToCheck.Where(x => x.ToCheck(onlyShowMissing).Count != 0))
{
bool currentCharacter = _clientState.LocalContentId == ch.Character.LocalContentId;
ImGui.BeginDisabled(currentCharacter);
if (ImGuiComponents.IconButton($"SwitchCharacters{ch.Character.LocalContentId}",
FontAwesomeIcon.DoorOpen))
{
_commandManager.ProcessCommand(
$"/ays relog {ch.Character.CharacterName}@{ch.Character.WorldName}");
}
ImGui.EndDisabled();
ImGui.SameLine();
if (currentCharacter)
ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen);
bool expanded = ImGui.CollapsingHeader($"{ch.Character}###GatheredCh{ch.Character.LocalContentId}");
if (currentCharacter)
ImGui.PopStyleColor();
if (expanded)
{
ImGui.Indent(_configWindow.MainIndentSize + ImGui.GetStyle().FramePadding.X);
foreach (var item in itemsToCheck.Where(x =>
ch.ToCheck(onlyShowMissing).ContainsKey(x.ItemId)))
{
var color = ch.Items[item.ItemId];
if (color != ColorGrey)
{
string itemName = item.GatheredItem.Name;
var folkloreBook = _gameCache.FolkloreBooks.Values.FirstOrDefault(x =>
x.GatheringItemIds.Contains(item.GatheredItem.GatheredItemId));
if (folkloreBook != null && !ch.Character.UnlockedFolkloreBooks.Contains(folkloreBook.ItemId))
itemName += $" ({SeIconChar.Prohibited.ToIconString()} {folkloreBook.Name})";
ImGui.PushStyleColor(ImGuiCol.Text, color);
if (currentCharacter && color == ColorRed)
{
ImGui.Selectable(itemName);
if (ImGui.IsItemClicked(ImGuiMouseButton.Left))
{
uint classJob = _clientState.LocalPlayer!.ClassJob.Id;
if (classJob == 16)
_commandManager.ProcessCommand($"/gathermin {item.GatheredItem.Name}");
else if (classJob == 17)
_commandManager.ProcessCommand($"/gatherbtn {item.GatheredItem.Name}");
else if (classJob == 18)
_commandManager.ProcessCommand($"/gatherfish {item.GatheredItem.Name}");
else
_commandManager.ProcessCommand($"/gather {item.GatheredItem.Name}");
}
}
else
{
ImGui.Text(itemName);
}
ImGui.PopStyleColor();
}
}
ImGui.Unindent(_configWindow.MainIndentSize + ImGui.GetStyle().FramePadding.X);
}
}
}
else
{
foreach (var item in itemsToCheck.Where(x =>
charactersToCheck.Any(y => y.ToCheck(onlyShowMissing).ContainsKey(x.ItemId))))
{
var folkloreBook = _gameCache.FolkloreBooks.Values.FirstOrDefault(x =>
x.GatheringItemIds.Contains(item.GatheredItem.GatheredItemId));
if (ImGui.CollapsingHeader($"{item.GatheredItem.Name}##Gathered{item.GatheredItem.ItemId}"))
{
ImGui.Indent(_configWindow.MainIndentSize + ImGui.GetStyle().FramePadding.X);
foreach (var ch in charactersToCheck)
{
var color = ch.Items[item.ItemId];
if (color == ColorRed || (color == ColorGreen && !onlyShowMissing))
{
bool currentCharacter = _clientState.LocalContentId == ch.Character.LocalContentId;
if (currentCharacter)
{
ImGui.PushFont(UiBuilder.IconFont);
var pos = ImGui.GetCursorPos();
ImGui.SetCursorPos(pos with
{
X = pos.X - ImGui.CalcTextSize(CurrentCharPrefix).X - 5
});
ImGui.TextUnformatted(CurrentCharPrefix);
ImGui.SetCursorPos(pos);
ImGui.PopFont();
}
string characterName = ch.Character.ToString();
if (folkloreBook != null && !ch.Character.UnlockedFolkloreBooks.Contains(folkloreBook.ItemId))
characterName += $" ({SeIconChar.Prohibited.ToIconString()} {folkloreBook.Name})";
ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextUnformatted(characterName);
ImGui.PopStyleColor();
}
}
ImGui.Unindent(_configWindow.MainIndentSize + ImGui.GetStyle().FramePadding.X);
}
}
}
}
private sealed class CheckedCharacter
{
public CheckedCharacter(Configuration configuration, Configuration.CharacterConfiguration character,
List<CheckedItem> itemsToCheck)
{
Character = character;
List<Guid> itemListIds = new();
if (character.Type == Configuration.CharacterType.Standalone)
{
itemListIds = character.ItemListIds;
}
else if (character.Type == Configuration.CharacterType.PartOfCharacterGroup)
{
var group = configuration.CharacterGroups.SingleOrDefault(x => x.Id == character.CharacterGroupId);
if (group != null)
itemListIds = group.ItemListIds;
}
var itemIdsOnLists = itemListIds.Where(listId => listId != Guid.Empty)
.Select(listId => configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
.Where(list => list != null)
.SelectMany(list => list!.Items)
.Select(x => x.ItemId)
.ToList();
foreach (var item in itemsToCheck)
{
// check if the item is on any relevant list
if (!itemIdsOnLists.Contains(item.ItemId))
{
Items[item.ItemId] = ColorGrey;
continue;
}
// check if we are the correct job
bool enabled = character.Retainers.Any(x => item.Ventures.Any(v => v.MatchesJob(x.Job)));
if (enabled)
{
// do we have it gathered on this char?
if (character.GatheredItems.Contains(item.GatheredItem.GatheredItemId))
Items[item.ItemId] = ColorGreen;
else
Items[item.ItemId] = ColorRed;
}
else
Items[item.ItemId] = ColorGrey;
}
}
public Configuration.CharacterConfiguration Character { get; }
public Dictionary<uint, Vector4> Items { get; } = new();
public Dictionary<uint, Vector4> ToCheck(bool onlyShowMissing)
{
return Items
.Where(x => x.Value == ColorRed || (x.Value == ColorGreen && !onlyShowMissing))
.ToDictionary(x => x.Key, x => x.Value);
}
}
private sealed class CheckedItem
{
public required ItemToGather GatheredItem { get; init; }
public required List<Venture> Ventures { get; init; }
public required uint ItemId { get; init; }
}
}

View File

@ -0,0 +1,61 @@
using System;
using Dalamud.Interface.Components;
using Dalamud.Interface.Utility.Raii;
using ImGuiNET;
namespace ARControl.Windows.Config;
internal sealed class MiscTab : ITab
{
private readonly ConfigWindow _configWindow;
private readonly Configuration _configuration;
public MiscTab(ConfigWindow configWindow, Configuration configuration)
{
_configWindow = configWindow;
_configuration = configuration;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Misc###TabMisc");
if (!tab)
return;
ImGui.Text("Venture Settings");
ImGui.Spacing();
ImGui.SetNextItemWidth(130);
int venturesToKeep = _configuration.Misc.VenturesToKeep;
if (ImGui.InputInt("Minimum Ventures needed to assign retainers", ref venturesToKeep))
{
_configuration.Misc.VenturesToKeep = Math.Max(0, Math.Min(65000, venturesToKeep));
_configWindow.ShouldSave();
}
ImGui.SameLine();
ImGuiComponents.HelpMarker(
$"If you have less than {venturesToKeep} ventures, retainers will only be sent out for Quick Ventures (instead of picking the next item from the Venture List).");
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
ImGui.Text("User Interface Settings");
bool showAssignmentChatMessages = _configuration.ConfigUiOptions.ShowAssignmentChatMessages;
if (ImGui.Checkbox("Show chat message when assigning a venture to a retainer",
ref showAssignmentChatMessages))
{
_configuration.ConfigUiOptions.ShowAssignmentChatMessages = showAssignmentChatMessages;
_configWindow.ShouldSave();
}
bool showContents = _configuration.ConfigUiOptions.ShowVentureListContents;
if (ImGui.Checkbox("Show Venture List preview in Groups/Retainer tabs", ref showContents))
{
_configuration.ConfigUiOptions.ShowVentureListContents = showContents;
_configWindow.ShouldSave();
}
}
}

View File

@ -0,0 +1,200 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility.Raii;
using FFXIVClientStructs.FFXIV.Common.Math;
using ImGuiNET;
using LLib;
namespace ARControl.Windows.Config;
internal sealed class RetainersTab : ITab
{
private readonly ConfigWindow _configWindow;
private readonly Configuration _configuration;
private readonly IconCache _iconCache;
public RetainersTab(ConfigWindow configWindow, Configuration configuration, IconCache iconCache)
{
_configWindow = configWindow;
_configuration = configuration;
_iconCache = iconCache;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Retainers###TabRetainers");
if (!tab)
return;
foreach (var world in _configuration.Characters
.Where(x => x.Retainers.Any(y => y.Job != 0))
.OrderBy(x => x.LocalContentId)
.GroupBy(x => x.WorldName))
{
ImGui.CollapsingHeader(world.Key,
ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.Bullet);
foreach (var character in world)
{
ImGui.PushID($"Char{character.LocalContentId}");
ImGui.SetNextItemWidth(ImGui.GetFontSize() * 30);
Vector4 buttonColor = ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.FrameBg));
if (character is { Type: not Configuration.CharacterType.NotManaged, Retainers.Count: > 0 })
{
if (character.Retainers.All(x => x.Managed))
buttonColor = ImGuiColors.HealerGreen;
else if (character.Retainers.All(x => !x.Managed))
buttonColor = ImGuiColors.DalamudRed;
else
buttonColor = ImGuiColors.DalamudOrange;
}
if (ImGuiComponents.IconButton(FontAwesomeIcon.Book, buttonColor))
{
if (character.Type == Configuration.CharacterType.NotManaged)
{
character.Type = Configuration.CharacterType.Standalone;
character.CharacterGroupId = Guid.Empty;
}
else
{
character.Type = Configuration.CharacterType.NotManaged;
character.CharacterGroupId = Guid.Empty;
}
_configWindow.ShouldSave();
}
ImGui.SameLine();
if (ImGui.CollapsingHeader(
$"{character.CharacterName} {(character.Type != Configuration.CharacterType.NotManaged ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}"))
{
ImGui.Indent(_configWindow.MainIndentSize);
List<(Guid Id, string Name)> groups =
new List<(Guid Id, string Name)> { (Guid.Empty, "No Group (manually assign lists)") }
.Concat(_configuration.CharacterGroups.Select(x => (x.Id, x.Name)))
.ToList();
using (var tabBar = ImRaii.TabBar("CharOptions"))
{
if (tabBar)
{
if (character.Type != Configuration.CharacterType.NotManaged)
DrawVentureListTab(character, groups);
DrawCharacterRetainersTab(character);
}
}
ImGui.Unindent(_configWindow.MainIndentSize);
}
ImGui.PopID();
}
}
}
private void DrawVentureListTab(Configuration.CharacterConfiguration character, List<(Guid Id, string Name)> groups)
{
using var tab = ImRaii.TabItem("Venture Lists");
if (!tab)
return;
int groupIndex = 0;
if (character.Type == Configuration.CharacterType.PartOfCharacterGroup)
groupIndex = groups.FindIndex(x => x.Id == character.CharacterGroupId);
if (ImGui.Combo("Character Group", ref groupIndex, groups.Select(x => x.Name).ToArray(),
groups.Count))
{
if (groupIndex == 0)
{
character.Type = Configuration.CharacterType.Standalone;
character.CharacterGroupId = Guid.Empty;
}
else
{
character.Type = Configuration.CharacterType.PartOfCharacterGroup;
character.CharacterGroupId = groups[groupIndex].Id;
}
_configWindow.ShouldSave();
}
ImGui.Separator();
if (groupIndex == 0)
{
// ReSharper disable once NullCoalescingConditionIsAlwaysNotNullAccordingToAPIContract
character.ItemListIds ??= new();
_configWindow.DrawVentureListSelection(
character.LocalContentId.ToString(CultureInfo.InvariantCulture),
character.ItemListIds);
}
else
{
ImGui.TextWrapped($"Retainers will participate in the following lists:");
ImGui.Indent(_configWindow.MainIndentSize);
var group = _configuration.CharacterGroups.Single(
x => x.Id == groups[groupIndex].Id);
var lists = group.ItemListIds
.Where(listId => listId != Guid.Empty)
.Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
.Where(list => list != null)
.Cast<Configuration.ItemList>()
.ToList();
if (lists.Count > 0)
{
foreach (var list in lists)
ImGui.BulletText($"{list.Name}");
}
else
ImGui.TextColored(ImGuiColors.DalamudRed, "(None)");
ImGui.Unindent(_configWindow.MainIndentSize);
ImGui.Spacing();
}
}
private void DrawCharacterRetainersTab(Configuration.CharacterConfiguration character)
{
using var tab = ImRaii.TabItem("Retainers");
if (!tab)
return;
foreach (var retainer in character.Retainers.Where(x => x.Job > 0)
.OrderBy(x => x.DisplayOrder)
.ThenBy(x => x.RetainerContentId))
{
ImGui.BeginDisabled(retainer is { Managed: false, Level: < ConfigWindow.MinLevel });
bool managed = retainer.Managed;
IDalamudTextureWrap? icon = _iconCache.GetIcon(62000 + retainer.Job);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, ImGui.GetStyle().FramePadding.X);
}
if (ImGui.Checkbox(
$"{retainer.Name}###Retainer{retainer.Name}{retainer.RetainerContentId}",
ref managed))
{
retainer.Managed = managed;
_configWindow.ShouldSave();
}
ImGui.EndDisabled();
}
}
}

View File

@ -0,0 +1,632 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using System.Text;
using System.Text.RegularExpressions;
using ARControl.External;
using ARControl.GameData;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Textures.TextureWraps;
using Dalamud.Interface.Utility;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin.Services;
using ECommons;
using ECommons.ImGuiMethods;
using ImGuiNET;
using LLib;
namespace ARControl.Windows.Config;
internal sealed class VentureListTab : ITab
{
private static readonly string[] StockingTypeLabels = ["Collect Once", "Keep in Stock"];
private static readonly string[] PriorityLabels =
{ "Collect in order of the list", "Collect item with lowest inventory first" };
private static readonly Regex CountAndName = new(@"^(\d{1,5})x?\s+(.*)$", RegexOptions.Compiled);
private const FontAwesomeIcon WarningIcon = FontAwesomeIcon.ExclamationCircle;
private const FontAwesomeIcon ExcessCrystalsIcon = FontAwesomeIcon.Diamond;
private readonly ConfigWindow _configWindow;
private readonly Configuration _configuration;
private readonly GameCache _gameCache;
private readonly IconCache _iconCache;
private readonly DiscardHelperIpc _discardHelperIpc;
private readonly IPluginLog _pluginLog;
private string _searchString = string.Empty;
private (Guid, Guid)? _draggedItem;
private readonly Dictionary<Guid, TemporaryConfig> _currentEditPopups = new();
private TemporaryConfig _newList = new()
{
Name = string.Empty,
ListType = Configuration.ListType.CollectOneTime,
ListPriority = Configuration.ListPriority.InOrder,
CheckRetainerInventory = false,
};
public VentureListTab(ConfigWindow configWindow, Configuration configuration, GameCache gameCache,
IconCache iconCache, DiscardHelperIpc discardHelperIpc, IPluginLog pluginLog)
{
_configWindow = configWindow;
_configuration = configuration;
_gameCache = gameCache;
_iconCache = iconCache;
_discardHelperIpc = discardHelperIpc;
_pluginLog = pluginLog;
}
public void Draw()
{
using var tab = ImRaii.TabItem("Venture Lists###TabVentureLists");
if (!tab)
return;
Configuration.ItemList? listToDelete = null;
IReadOnlySet<uint> itemsToDiscard = _discardHelperIpc.GetItemsToDiscard();
foreach (var list in _configuration.ItemLists)
{
ImGui.PushID($"List{list.Id}");
if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog))
{
_currentEditPopups[list.Id] = new TemporaryConfig
{
Name = list.Name,
ListType = list.Type,
ListPriority = list.Priority,
CheckRetainerInventory = list.CheckRetainerInventory,
};
ImGui.OpenPopup($"##EditList{list.Id}");
}
DrawVentureListEditorPopup(list, ref listToDelete);
ImGui.SameLine();
string label = $"{list.Name} {list.GetIcon()}";
if (ImGui.CollapsingHeader(label))
{
ImGui.Indent(_configWindow.MainIndentSize);
DrawVentureListItemSelection(list, itemsToDiscard);
ImGui.Unindent(_configWindow.MainIndentSize);
}
ImGui.PopID();
}
if (listToDelete != null)
{
_configuration.ItemLists.Remove(listToDelete);
_configWindow.ShouldSave();
}
ImGui.Separator();
DrawNewVentureList();
}
private void DrawVentureListEditorPopup(Configuration.ItemList list, ref Configuration.ItemList? listToDelete)
{
var assignedCharacters = _configuration.Characters
.Where(x => x.Type == Configuration.CharacterType.Standalone && x.ItemListIds.Contains(list.Id))
.OrderBy(x => x.WorldName)
.ThenBy(x => x.LocalContentId)
.ToList();
var assignedGroups = _configuration.CharacterGroups
.Where(x => x.ItemListIds.Contains(list.Id))
.ToList();
if (_currentEditPopups.TryGetValue(list.Id, out TemporaryConfig? temporaryConfig) &&
ImGui.BeginPopup($"##EditList{list.Id}"))
{
var (save, canSave) = DrawVentureListEditor(temporaryConfig, list);
ImGui.BeginDisabled(!canSave || (list.Name == temporaryConfig.Name &&
list.Type == temporaryConfig.ListType &&
list.Priority == temporaryConfig.ListPriority &&
list.CheckRetainerInventory ==
temporaryConfig.CheckRetainerInventory));
save |= ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save");
ImGui.EndDisabled();
if (save && canSave)
{
list.Name = temporaryConfig.Name;
list.Type = temporaryConfig.ListType;
if (list.Type == Configuration.ListType.CollectOneTime)
{
list.Priority = Configuration.ListPriority.InOrder;
list.CheckRetainerInventory = false;
}
else
{
list.Priority = temporaryConfig.ListPriority;
list.CheckRetainerInventory = temporaryConfig.CheckRetainerInventory;
}
ImGui.CloseCurrentPopup();
_configWindow.ShouldSave();
}
else
{
ImGui.SameLine();
ImGui.BeginDisabled(assignedCharacters.Count > 0 || assignedGroups.Count > 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Times, "Delete"))
{
listToDelete = list;
ImGui.CloseCurrentPopup();
}
ImGui.EndDisabled();
if ((assignedCharacters.Count > 0 || assignedGroups.Count > 0) &&
ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
{
ImGui.BeginTooltip();
ImGui.Text(
$"Remove this list from the {assignedCharacters.Count} character(s) and {assignedGroups.Count} group(s) using it before deleting it.");
foreach (var character in assignedCharacters)
ImGui.BulletText($"{character.CharacterName} @ {character.WorldName}");
foreach (var group in assignedGroups)
ImGui.BulletText($"{group.Name}");
ImGui.EndTooltip();
}
}
ImGui.EndPopup();
}
}
private void DrawVentureListItemSelection(Configuration.ItemList list, IReadOnlySet<uint> itemsToDiscard)
{
DrawVentureListItemFilter(list);
Configuration.QueuedItem? itemToRemove = null;
Configuration.QueuedItem? itemToAdd = null;
int indexToAdd = 0;
float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < list.Items.Count; ++i)
{
Vector2 topLeft = ImGui.GetCursorScreenPos() +
new Vector2(-_configWindow.MainIndentSize, -ImGui.GetStyle().ItemSpacing.Y / 2);
var item = list.Items[i];
ImGui.PushID($"QueueItem{item.InternalId}");
var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList();
var venture = ventures.First();
if (itemsToDiscard.Contains(venture.ItemId))
DrawWarning(WarningIcon, "This item will be automatically discarded by 'Discard Helper'.");
else if (item.ItemId is >= 2 and <= 13 && item.RemainingQuantity >= 10000)
{
if (list.Type == Configuration.ListType.CollectOneTime || list.CheckRetainerInventory)
DrawWarning(ExcessCrystalsIcon,
"You are responsible for manually moving shards or crystals to your retainers - ARC won't do that for you.\nIf you don't, this may lead to wasted ventures.",
ImGuiColors.ParsedBlue);
else
DrawWarning(WarningIcon, "You can never have this many of a shard or crystal in your inventory.");
}
IDalamudTextureWrap? icon = _iconCache.GetIcon(venture.IconId);
if (icon != null)
{
ImGui.Image(icon.ImGuiHandle, new Vector2(ImGui.GetFrameHeight()));
ImGui.SameLine(0, ImGui.GetStyle().FramePadding.X);
}
ImGui.SetNextItemWidth(130 * ImGuiHelpers.GlobalScale);
int quantity = item.RemainingQuantity;
if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})",
ref quantity, 100))
{
item.RemainingQuantity = quantity;
_configWindow.ShouldSave();
}
if (list.Items.Count > 1)
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
_configWindow.MainIndentSize +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 4 -
ImGui.GetStyle().ItemSpacing.X);
ImGui.PopFont();
if (_draggedItem != null && _draggedItem.Value.Item1 == list.Id &&
_draggedItem.Value.Item2 == item.InternalId)
{
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown,
ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = (list.Id, item.InternalId);
ImGui.SameLine();
}
else
{
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(ImGui.GetContentRegionAvail().X +
_configWindow.MainIndentSize +
ImGui.GetStyle().WindowPadding.X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 2);
ImGui.PopFont();
}
if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
itemToRemove = item;
ImGui.PopID();
Vector2 bottomRight = new Vector2(topLeft.X + width + _configWindow.MainIndentSize,
ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2);
//ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(i % 2 == 0 ? ImGuiColors.DalamudRed : ImGuiColors.HealerGreen));
itemPositions.Add((topLeft, bottomRight));
}
if (!ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = null;
else if (_draggedItem != null && _draggedItem.Value.Item1 == list.Id)
{
var draggedItem = list.Items.Single(x => x.InternalId == _draggedItem.Value.Item2);
int oldIndex = list.Items.IndexOf(draggedItem);
var (topLeft, bottomRight) = itemPositions[oldIndex];
if (!itemsToDiscard.Contains(draggedItem.ItemId))
topLeft += new Vector2(_configWindow.MainIndentSize, 0);
ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f,
ImDrawFlags.RoundCornersAll);
int newIndex = itemPositions.FindIndex(x => ImGui.IsMouseHoveringRect(x.TopLeft, x.BottomRight, true));
if (newIndex >= 0 && oldIndex != newIndex)
{
itemToAdd = list.Items.Single(x => x.InternalId == _draggedItem.Value.Item2);
indexToAdd = newIndex;
}
}
if (itemToRemove != null)
{
list.Items.Remove(itemToRemove);
_configWindow.ShouldSave();
}
if (itemToAdd != null)
{
_pluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}");
list.Items.Remove(itemToAdd);
list.Items.Insert(indexToAdd, itemToAdd);
_configWindow.ShouldSave();
}
ImGui.Spacing();
List<Configuration.QueuedItem> clipboardItems = ParseClipboardItems();
ImportFromClipboardButton(list, clipboardItems);
RemoveFinishedItemsButton(list);
ImGui.Spacing();
}
private static void DrawWarning(FontAwesomeIcon icon, string tooltip, Vector4? color = null)
{
ImGui.PushFont(UiBuilder.IconFont);
var pos = ImGui.GetCursorPos();
ImGui.SetCursorPos(new Vector2(pos.X - ImGui.CalcTextSize(icon.ToIconString()).X - 5, pos.Y + 2));
ImGui.TextColored(color ?? ImGuiColors.DalamudYellow, icon.ToIconString());
ImGui.SetCursorPos(pos);
ImGui.PopFont();
if (ImGui.IsItemHovered())
ImGui.SetTooltip(tooltip);
}
private void DrawNewVentureList()
{
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Venture List"))
ImGui.OpenPopup("##AddList");
if (ImGui.BeginPopup("##AddList"))
{
(bool save, bool canSave) = DrawVentureListEditor(_newList, null);
ImGui.BeginDisabled(!canSave);
save |= ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Save, "Save");
ImGui.EndDisabled();
if (save && canSave)
{
_configuration.ItemLists.Add(new Configuration.ItemList
{
Id = Guid.NewGuid(),
Name = _newList.Name,
Type = _newList.ListType,
Priority = _newList.ListPriority,
CheckRetainerInventory = _newList.CheckRetainerInventory,
});
_newList = new()
{
Name = string.Empty,
ListType = Configuration.ListType.CollectOneTime,
ListPriority = Configuration.ListPriority.InOrder,
CheckRetainerInventory = false,
};
ImGui.CloseCurrentPopup();
_configWindow.ShouldSave();
}
ImGui.EndPopup();
}
}
private (bool Save, bool CanSave) DrawVentureListEditor(TemporaryConfig temporaryConfig,
Configuration.ItemList? list)
{
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
string listName = temporaryConfig.Name;
bool save = ImGui.InputTextWithHint("", "List Name...", ref listName, 64,
ImGuiInputTextFlags.EnterReturnsTrue);
bool canSave = IsValidListName(listName, list);
temporaryConfig.Name = listName;
ImGui.PushID($"Type{list?.Id ?? Guid.Empty}");
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
int type = (int)temporaryConfig.ListType;
if (ImGui.Combo("", ref type, StockingTypeLabels, StockingTypeLabels.Length))
{
temporaryConfig.ListType = (Configuration.ListType)type;
if (temporaryConfig.ListType == Configuration.ListType.CollectOneTime)
temporaryConfig.ListPriority = Configuration.ListPriority.InOrder;
}
ImGui.PopID();
if (temporaryConfig.ListType == Configuration.ListType.KeepStocked)
{
ImGui.PushID($"Priority{list?.Id ?? Guid.Empty}");
ImGui.SetNextItemWidth(375 * ImGuiHelpers.GlobalScale);
int priority = (int)temporaryConfig.ListPriority;
if (ImGui.Combo("", ref priority, PriorityLabels, PriorityLabels.Length))
temporaryConfig.ListPriority = (Configuration.ListPriority)priority;
ImGui.PopID();
ImGui.PushID($"CheckRetainerInventory{list?.Id ?? Guid.Empty}");
bool checkRetainerInventory = temporaryConfig.CheckRetainerInventory;
if (ImGui.Checkbox("Check Retainer Inventory for items (requires AllaganTools)",
ref checkRetainerInventory))
temporaryConfig.CheckRetainerInventory = checkRetainerInventory;
ImGui.PopID();
}
return (save, canSave);
}
private void DrawVentureListItemFilter(Configuration.ItemList list)
{
ImGuiEx.SetNextItemFullWidth();
if (ImGui.BeginCombo($"##VentureSelection{list.Id}", "Add Venture...", ImGuiComboFlags.HeightLarge))
{
ImGuiEx.SetNextItemFullWidth();
bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256,
ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue);
int quantity;
string itemName;
var regexMatch = CountAndName.Match(_searchString);
if (regexMatch.Success)
{
quantity = int.Parse(regexMatch.Groups[1].Value, CultureInfo.InvariantCulture);
itemName = regexMatch.Groups[2].Value;
}
else
{
quantity = 0;
itemName = _searchString;
}
foreach (var filtered in _gameCache.Ventures
.Where(x => x.Name.Contains(itemName, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.Level)
.ThenBy(x => x.Name)
.ThenBy(x => x.ItemId)
.GroupBy(x => x.ItemId)
.Select(x => new
{
Venture = x.First(),
CategoryNames = x.Select(y => y.CategoryName)
}))
{
IDalamudTextureWrap? icon = _iconCache.GetIcon(filtered.Venture.IconId);
Vector2 pos = ImGui.GetCursorPos();
Vector2 iconSize = new Vector2(ImGui.GetTextLineHeight() + ImGui.GetStyle().ItemSpacing.Y);
if (icon != null)
{
ImGui.SetCursorPos(pos + new Vector2(iconSize.X + ImGui.GetStyle().FramePadding.X,
ImGui.GetStyle().ItemSpacing.Y / 2));
}
bool addThis = ImGui.Selectable(
$"{filtered.Venture.Name} ({string.Join(" ", filtered.CategoryNames)})##SelectVenture{filtered.Venture.RowId}");
if (icon != null)
{
ImGui.SameLine(0, 0);
ImGui.SetCursorPos(pos);
ImGui.Image(icon.ImGuiHandle, iconSize);
icon.Dispose();
}
if (addThis || addFirst)
{
list.Items.Add(new Configuration.QueuedItem
{
ItemId = filtered.Venture.ItemId,
RemainingQuantity = quantity,
});
if (addFirst)
{
addFirst = false;
ImGui.CloseCurrentPopup();
}
_configWindow.ShouldSave();
}
}
ImGui.EndCombo();
}
ImGui.Spacing();
}
private void ImportFromClipboardButton(Configuration.ItemList list, List<Configuration.QueuedItem> clipboardItems)
{
ImGui.BeginDisabled(clipboardItems.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Download, "Import from Clipboard"))
{
_pluginLog.Information($"Importing {clipboardItems.Count} clipboard items");
foreach (var item in clipboardItems)
{
var existingItem = list.Items.FirstOrDefault(x => x.ItemId == item.ItemId);
if (existingItem != null)
existingItem.RemainingQuantity += item.RemainingQuantity;
else
list.Items.Add(item);
}
_configWindow.ShouldSave();
}
ImGui.EndDisabled();
if (ImGui.IsItemHovered())
{
ImGui.BeginTooltip();
ImGui.Text("Supports importing a list in a Teamcraft-compatible format.");
ImGui.Spacing();
if (clipboardItems.Count > 0)
{
ImGui.Text("Clicking this button now would add the following items:");
ImGui.Indent();
foreach (var item in clipboardItems)
ImGui.TextUnformatted(
$"{item.RemainingQuantity}x {_gameCache.Ventures.First(x => item.ItemId == x.ItemId).Name}");
ImGui.Unindent();
}
else
{
ImGui.Text("For example:");
ImGui.Indent();
ImGui.Text("2000x Cobalt Ore");
ImGui.Text("1000x Gold Ore");
ImGui.Unindent();
}
ImGui.EndTooltip();
}
}
private void RemoveFinishedItemsButton(Configuration.ItemList list)
{
if (list.Items.Count > 0 && list.Type == Configuration.ListType.CollectOneTime)
{
ImGui.SameLine();
ImGui.BeginDisabled(list.Items.All(x => x.RemainingQuantity > 0));
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove all finished items"))
{
list.Items.RemoveAll(q => q.RemainingQuantity <= 0);
_configWindow.ShouldSave();
}
ImGui.EndDisabled();
}
}
private List<Configuration.QueuedItem> ParseClipboardItems()
{
List<Configuration.QueuedItem> clipboardItems = new List<Configuration.QueuedItem>();
try
{
string? clipboardText = GetClipboardText();
if (!string.IsNullOrWhiteSpace(clipboardText))
{
foreach (var clipboardLine in clipboardText.ReplaceLineEndings().Split(Environment.NewLine))
{
var match = CountAndName.Match(clipboardLine);
if (!match.Success)
continue;
var venture = _gameCache.Ventures.FirstOrDefault(x =>
x.Name.Equals(match.Groups[2].Value, StringComparison.OrdinalIgnoreCase));
if (venture != null && int.TryParse(match.Groups[1].Value, out int quantity))
{
clipboardItems.Add(new Configuration.QueuedItem
{
ItemId = venture.ItemId,
RemainingQuantity = quantity,
});
}
}
}
}
catch (Exception e)
{
_pluginLog.Warning(e, "Unable to extract clipboard text");
}
return clipboardItems;
}
private bool IsValidListName(string name, Configuration.ItemList? existingList)
{
return name.Length >= 2 &&
!name.Contains('%', StringComparison.Ordinal) &&
!_configuration.ItemLists.Any(x => x != existingList && name.EqualsIgnoreCase(x.Name));
}
/// <summary>
/// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
/// </summary>
private unsafe string? GetClipboardText()
{
byte* ptr = ImGuiNative.igGetClipboardText();
if (ptr == null)
return null;
int byteCount = 0;
while (ptr[byteCount] != 0)
++byteCount;
return Encoding.UTF8.GetString(ptr, byteCount);
}
private sealed class TemporaryConfig
{
public required string Name { get; set; }
public Configuration.ListType ListType { get; set; }
public Configuration.ListPriority ListPriority { get; set; }
public bool CheckRetainerInventory { get; set; }
}
}

File diff suppressed because it is too large Load Diff

View File

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

1
AutoRetainerAPI Submodule

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

1
ECommons Submodule

@ -0,0 +1 @@
Subproject commit 677e28c0696eb13351d90d13ff27adb667b2c862

2
LLib

@ -1 +1 @@
Subproject commit abbbec4f26b1a8903b0cd7aa04f00d557602eaf3 Subproject commit fa3d19dde18bfd00237e66ea1eb48a60caa01b8a

View File

@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "7.0.0", "version": "8.0.0",
"rollForward": "latestMinor", "rollForward": "latestMinor",
"allowPrerelease": false "allowPrerelease": false
} }