Compare commits

..

50 Commits
v1.0 ... 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
37bdefcc55
Change log levels 2023-10-12 11:07:52 +02:00
2cd9013e3e
Bump Version for repo 2023-10-12 02:13:58 +02:00
7676e3b2b1
Bump AR 2023-10-12 01:41:22 +02:00
c7b5a7bee2
Merge branch 'venture-2.x' 2023-10-12 01:40:36 +02:00
2f65d7c951
Replace overlay icon 2023-10-11 10:42:20 +02:00
fbba9244a6
(2.x) Improve chat feedback 2023-10-11 10:39:43 +02:00
fe29867929
Add LLib 2023-10-11 10:19:58 +02:00
7332f9945f
Change the minimum level for retainers to work 2023-10-10 16:29:44 +02:00
d8cfb75c33
(2.x) Pick appropriate venture for new list types/priorities 2023-10-10 16:27:10 +02:00
59d6f50c18
(2.x) Add icons 2023-10-10 13:26:25 +02:00
6564a49369
(2.x) Update Lists/locked items tabs 2023-10-10 12:08:26 +02:00
189d4fb0a7
(2.x) Update Retainer/Group config tabs 2023-10-09 01:48:27 +02:00
2f8fb54e70
Update Icon 2023-10-05 16:53:29 +02:00
6645241651
Clean up dependencies 2023-10-04 12:32:11 +02:00
30 changed files with 3798 additions and 489 deletions

9
.gitmodules vendored Normal file
View File

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

View File

@ -2,6 +2,12 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARControl.csproj", "{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARControl.csproj", "{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{C00249D7-E550-4A3F-937B-D938D1D46B8A}"
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
@ -12,5 +18,17 @@ Global
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.Build.0 = Debug|Any CPU {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.ActiveCfg = Release|Any CPU {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.Build.0 = Release|Any CPU {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.Build.0 = Release|Any CPU
{C00249D7-E550-4A3F-937B-D938D1D46B8A}.Debug|Any CPU.ActiveCfg = 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.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>1.0</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.0.2\</AutoRetainerLibPath>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))'">
<DalamudLibPath>$(DALAMUD_HOME)/</DalamudLibPath>
</PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/> <ProjectReference Include="..\AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj" />
<ProjectReference Include="..\LLib\LLib.csproj"/>
</ItemGroup> </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>
<Reference Include="ClickLib">
<HintPath>$(AutoRetainerLibPath)ClickLib.dll</HintPath>
</Reference>
</ItemGroup>
<Target Name="RenameLatestZip" AfterTargets="PackagePlugin">
<Exec Command="rename $(OutDir)$(AssemblyName)\latest.zip $(AssemblyName)-$(Version).zip"/>
</Target>
</Project> </Project>

View File

@ -3,5 +3,6 @@
"Author": "Liza Carvelli", "Author": "Liza Carvelli",
"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://plugins.carvel.li/icons/ARControl.png"
} }

View File

@ -1,4 +1,5 @@
using System.Linq; using System.Collections.Generic;
using System.Linq;
namespace ARControl; namespace ARControl;
@ -24,7 +25,7 @@ partial class AutoRetainerControlPlugin
LocalContentId = registeredCharacterId, LocalContentId = registeredCharacterId,
CharacterName = offlineCharacterData.Name, CharacterName = offlineCharacterData.Name,
WorldName = offlineCharacterData.World, WorldName = offlineCharacterData.World,
Managed = false, Type = Configuration.CharacterType.NotManaged,
}; };
save = true; save = true;
@ -37,13 +38,63 @@ partial class AutoRetainerControlPlugin
save = true; save = true;
} }
foreach (var retainerData in offlineCharacterData.RetainerData) if (character.Ventures != offlineCharacterData.Ventures)
{ {
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,
}; };
@ -52,6 +103,12 @@ partial class AutoRetainerControlPlugin
character.Retainers.Add(retainer); character.Retainers.Add(retainer);
} }
if (retainer.Name != retainerData.Name)
{
retainer.Name = retainerData.Name;
save = true;
}
if (retainer.DisplayOrder != retainerData.DisplayOrder) if (retainer.DisplayOrder != retainerData.DisplayOrder)
{ {
retainer.DisplayOrder = retainerData.DisplayOrder; retainer.DisplayOrder = retainerData.DisplayOrder;
@ -70,6 +127,12 @@ partial class AutoRetainerControlPlugin
save = true; save = true;
} }
if (retainer.HasVenture != retainerData.HasVenture)
{
retainer.HasVenture = retainerData.HasVenture;
save = true;
}
if (retainer.LastVenture != retainerData.VentureID) if (retainer.LastVenture != retainerData.VentureID)
{ {
retainer.LastVenture = retainerData.VentureID; retainer.LastVenture = retainerData.VentureID;
@ -96,6 +159,17 @@ partial class AutoRetainerControlPlugin
save = true; save = true;
} }
} }
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;
}
} }
if (save) if (save)

View File

@ -1,26 +1,32 @@
using System; using System;
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;
using Dalamud.Game.Command; using Dalamud.Game.Command;
using Dalamud.Interface; using Dalamud.Game.Text;
using Dalamud.Interface.Components; using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
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 ImGuiNET; using ImGuiNET;
using LLib;
namespace ARControl; namespace ARControl;
[SuppressMessage("ReSharper", "UnusedType.Global")] [SuppressMessage("ReSharper", "UnusedType.Global")]
public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{ {
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;
@ -28,28 +34,41 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly GameCache _gameCache; private readonly GameCache _gameCache;
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, IPluginLog pluginLog) IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider,
IFramework framework, IPluginLog pluginLog)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_clientState = clientState; _clientState = clientState;
_chatGui = chatGui; _chatGui = chatGui;
_commandManager = commandManager; _commandManager = commandManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration { Version = 2 };
_gameCache = new GameCache(dataManager); _gameCache = new GameCache(dataManager);
_iconCache = new IconCache(textureProvider);
_ventureResolver = new VentureResolver(_gameCache, _pluginLog); _ventureResolver = new VentureResolver(_gameCache, _pluginLog);
_configWindow = new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _pluginLog); DiscardHelperIpc discardHelperIpc = new(_pluginInterface);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog);
_configWindow =
new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache,
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;
@ -62,81 +81,295 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
}); });
if (_autoRetainerApi.Ready) if (_autoRetainerApi.Ready)
{
try
{
Sync(); 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);
if (venture.HasValue)
_autoRetainerApi.SetVenture(venture.Value);
}
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)
{ {
_pluginLog.Information("No character information found"); _pluginLog.Information("No character information found");
return null;
} }
else if (!ch.Managed)
if (ch.Type == Configuration.CharacterType.NotManaged)
{ {
_pluginLog.Information("Character is not managed"); _pluginLog.Information("Character is not managed");
return null;
} }
else
{
var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName); var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName);
if (retainer == null) if (retainer == null)
{ {
_pluginLog.Information("No retainer information found"); _pluginLog.Information("No retainer information found");
return null;
} }
else if (!retainer.Managed)
if (!retainer.Managed)
{ {
_pluginLog.Information("Retainer is not managed"); _pluginLog.Information("Retainer is not managed");
return null;
} }
else
{
_pluginLog.Information("Checking tasks..."); _pluginLog.Information("Checking tasks...");
Sync(); Sync();
foreach (var queuedItem in _configuration.QueuedItems.Where(x => x.RemainingQuantity > 0))
{
_pluginLog.Information($"Checking venture info for itemId {queuedItem.ItemId}");
var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, queuedItem); if (ch.Ventures == 0)
if (reward == null)
{ {
_pluginLog.Information("Retainer can't complete venture"); _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 venturesInProgress = CalculateVenturesInProgress(ch);
foreach (var inProgress in venturesInProgress)
{
_pluginLog.Verbose(
$"Venture In Progress: ItemId {inProgress.Key} for a total amount of {inProgress.Value}");
}
IReadOnlyList<Guid> itemListIds;
if (ch.Type == Configuration.CharacterType.Standalone)
itemListIds = ch.ItemListIds;
else
{
var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId);
if (group == null)
{
_pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}.");
return null;
}
itemListIds = group.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
{
itemsOnList = list.Items
.Select(x => new StockedItem
{
QueuedItem = x,
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);
}
return venture.RowId;
}
}
}
}
// fallback: managed but no venture found/
if (retainer.LastVenture != QuickVentureId)
{
PrintEndOfListMessage(retainerName, retainer);
if (!dryRun)
{
retainer.HasVenture = true;
retainer.LastVenture = QuickVentureId;
_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;
}
else
{
_pluginLog.Information("Not changing venture, already a quick venture");
return null;
}
}
private void PrintNextVentureMessage(string retainerName, Venture venture, VentureReward reward, Configuration.ItemList list)
{ {
_chatGui.Print( _chatGui.Print(
$"[ARC] Sending retainer {retainerName} to collect {reward.Quantity}x {venture!.Name}."); 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( _pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}"); $"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
_autoRetainerApi.SetVenture(venture.RowId);
retainer.LastVenture = venture.RowId;
queuedItem.RemainingQuantity =
Math.Max(0, queuedItem.RemainingQuantity - reward.Quantity);
_pluginInterface.SavePluginConfig(_configuration);
return;
}
} }
// fallback: managed but no venture found private void PrintEndOfListMessage(string retainerName, Configuration.RetainerConfiguration retainer)
if (retainer.LastVenture != 395)
{ {
_chatGui.Print($"[ARC] No tasks left for retainer {retainerName}, sending to Quick Venture."); _chatGui.Print(
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QC"); new SeString(new UIForegroundPayload(579))
_autoRetainerApi.SetVenture(395); .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");
}
retainer.LastVenture = 395; /// <remarks>
_pluginInterface.SavePluginConfig(_configuration); /// 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.
/// </remarks>
private Dictionary<uint, int> CalculateVenturesInProgress(Configuration.CharacterConfiguration character)
{
Dictionary<uint, int> inProgress = new Dictionary<uint, int>();
foreach (var retainer in character.Retainers)
{
if (retainer.Managed && retainer.HasVenture && retainer.LastVenture != 0)
{
uint ventureId = retainer.LastVenture;
if (ventureId == 0)
continue;
var ventureForId = _gameCache.Ventures.SingleOrDefault(x => x.RowId == ventureId);
if (ventureForId == null)
continue;
uint itemId = ventureForId.ItemId;
var (venture, reward) = _ventureResolver.ResolveVenture(character, retainer, itemId);
if (venture == null || reward == null)
continue;
if (inProgress.TryGetValue(itemId, out int existingQuantity))
inProgress[itemId] = reward.Quantity + existingQuantity;
else else
_pluginLog.Information("Not changing venture plan, already 395"); inProgress[itemId] = reward.Quantity;
} }
} }
return inProgress;
} }
private void RetainerTaskButtonDraw(ulong characterId, string retainerName) private void RetainerTaskButtonDraw(ulong characterId, string retainerName)
{ {
Configuration.CharacterConfiguration? characterConfiguration = Configuration.CharacterConfiguration? characterConfiguration =
_configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId); _configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId);
if (characterConfiguration is not { Managed: true }) if (characterConfiguration is not { Type: not Configuration.CharacterType.NotManaged })
return; return;
Configuration.RetainerConfiguration? retainer = Configuration.RetainerConfiguration? retainer =
@ -145,7 +378,19 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
return; return;
ImGui.SameLine(); ImGui.SameLine();
ImGuiComponents.IconButton(FontAwesomeIcon.Book); ImGui.Text(SeIconChar.Collectible.ToIconString());
if (ImGui.IsItemHovered())
{
string text = "This retainer is managed by ARC.";
if (characterConfiguration.Type == Configuration.CharacterType.PartOfCharacterGroup)
{
var group = _configuration.CharacterGroups.Single(x => x.Id == characterConfiguration.CharacterGroupId);
text += $"\n\nCharacter Group: {group.Name}";
}
ImGui.SetTooltip(text);
}
} }
private void TerritoryChanged(ushort e) => Sync(); private void TerritoryChanged(ushort e) => Sync();
@ -154,6 +399,43 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{ {
if (arguments == "sync") if (arguments == "sync")
Sync(); Sync();
else if (arguments.StartsWith("dnv", StringComparison.Ordinal))
{
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null || ch.Type == Configuration.CharacterType.NotManaged || ch.Retainers.Count == 0)
{
_chatGui.PrintError("No character to debug.");
return;
}
string[] s = arguments.Split(" ");
string? retainerName;
if (s.Length > 1)
retainerName = ch.Retainers.SingleOrDefault(x => x.Name.EqualsIgnoreCase(s[1]))?.Name;
else
retainerName = ch.Retainers
.OrderBy(x => x.DisplayOrder)
.ThenBy(x => x.RetainerContentId)
.FirstOrDefault()?.Name;
if (retainerName == null)
{
if (s.Length > 1)
_chatGui.PrintError($"Could not find retainer {s[1]}.");
else
_chatGui.PrintError("Could not find retainer.");
return;
}
var venture = GetNextVenture(retainerName, true);
if (venture == QuickVentureId)
_chatGui.Print($"Next venture for {retainerName} is Quick Venture.");
else if (venture.HasValue)
_chatGui.Print(
$"Next venture for {retainerName} is {_gameCache.Ventures.First(x => x.RowId == venture.Value).Name}.");
else
_chatGui.Print($"Next venture for {retainerName} is (none).");
}
else else
_configWindow.Toggle(); _configWindow.Toggle();
} }
@ -167,7 +449,22 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_iconCache.Dispose();
_autoRetainerReflection.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();
ECommonsMain.Dispose(); ECommonsMain.Dispose();
} }
private sealed class StockedItem
{
public required Configuration.QueuedItem QueuedItem { get; set; }
public required int InventoryCount { get; set; }
public uint ItemId => QueuedItem.ItemId;
public int RequestedCount
{
get => QueuedItem.RemainingQuantity;
set => QueuedItem.RemainingQuantity = value;
}
}
} }

View File

@ -1,44 +1,131 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.Text;
using Newtonsoft.Json;
namespace ARControl; namespace ARControl;
internal sealed class Configuration : IPluginConfiguration internal sealed class Configuration : IPluginConfiguration
{ {
public int Version { get; set; } = 1; public int Version { get; set; }
public List<QueuedItem> QueuedItems { get; set; } = new();
public List<CharacterConfiguration> Characters { get; set; } = new(); public List<CharacterConfiguration> Characters { get; set; } = new();
public List<ItemList> ItemLists { 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 required Guid Id { get; set; }
public required string Name { get; set; }
public required ListType Type { get; set; } = ListType.CollectOneTime;
public required ListPriority Priority { get; set; } = ListPriority.InOrder;
public bool CheckRetainerInventory { get; set; }
public List<QueuedItem> Items { get; set; } = new();
public string GetIcon()
{
if (Id == Guid.Empty)
return string.Empty;
return Type switch
{
ListType.CollectOneTime => SeIconChar.BoxedNumber1.ToIconString(),
ListType.KeepStocked when Priority == ListPriority.Balanced => SeIconChar.EurekaLevel.ToIconString(),
ListType.KeepStocked => SeIconChar.Circle.ToIconString(),
_ => string.Empty
};
}
}
public enum ListType
{
CollectOneTime,
KeepStocked,
}
public enum ListPriority
{
InOrder,
Balanced,
}
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; }
} }
public sealed class CharacterGroup
{
public required Guid Id { get; set; }
public required string Name { get; set; }
public List<Guid> ItemListIds { get; set; } = new();
}
public sealed class CharacterConfiguration public sealed class CharacterConfiguration
{ {
public required ulong LocalContentId { get; set; } public required ulong LocalContentId { get; set; }
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 required bool Managed { get; set; }
public uint Ventures { get; set; }
public CharacterType Type { get; set; } = CharacterType.NotManaged;
public Guid CharacterGroupId { get; set; }
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}";
} }
public enum CharacterType
{
NotManaged,
/// <summary>
/// The character's item list(s) are manually selected.
/// </summary>
Standalone,
/// <summary>
/// All item lists are managed through the character group.
/// </summary>
PartOfCharacterGroup
}
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; }
public int Level { get; set; } public int Level { get; set; }
public uint Job { get; set; } public uint Job { get; set; }
public bool HasVenture { get; set; }
public uint LastVenture { get; set; } public uint LastVenture { get; set; }
public int ItemLevel { get; set; } public int ItemLevel { get; set; }
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;
}
} }

View File

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

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

@ -14,6 +14,7 @@ internal sealed class Venture
var taskDetails = dataManager.GetExcelSheet<RetainerTaskNormal>()!.GetRow(retainerTask.Task)!; var taskDetails = dataManager.GetExcelSheet<RetainerTaskNormal>()!.GetRow(retainerTask.Task)!;
var taskParameters = retainerTask.RetainerTaskParameter.Value!; var taskParameters = retainerTask.RetainerTaskParameter.Value!;
ItemId = taskDetails.Item.Row; ItemId = taskDetails.Item.Row;
IconId = taskDetails.Item.Value!.Icon;
Name = taskDetails.Item.Value!.Name.ToString(); Name = taskDetails.Item.Value!.Name.ToString();
Level = retainerTask.RetainerLevel; Level = retainerTask.RetainerLevel;
ItemLevelCombat = retainerTask.RequiredItemLevel; ItemLevelCombat = retainerTask.RequiredItemLevel;
@ -76,6 +77,7 @@ internal sealed class Venture
} }
public uint ItemId { get; } public uint ItemId { get; }
public ushort IconId { get; }
public string Name { get; } public string Name { get; }
public byte Level { get; } public byte Level { get; }
public ushort ItemLevelCombat { get; } public ushort ItemLevelCombat { get; }

View File

@ -15,26 +15,26 @@ internal sealed class VentureResolver
} }
public (Venture?, VentureReward?) ResolveVenture(Configuration.CharacterConfiguration character, public (Venture?, VentureReward?) ResolveVenture(Configuration.CharacterConfiguration character,
Configuration.RetainerConfiguration retainer, Configuration.QueuedItem queuedItem) Configuration.RetainerConfiguration retainer, uint itemId)
{ {
var venture = _gameCache.Ventures var venture = _gameCache.Ventures
.Where(x => retainer.Level >= x.Level) .Where(x => retainer.Level >= x.Level)
.FirstOrDefault(x => x.ItemId == queuedItem.ItemId && x.MatchesJob(retainer.Job)); .FirstOrDefault(x => x.ItemId == itemId && x.MatchesJob(retainer.Job));
if (venture == null) if (venture == null)
{ {
_pluginLog.Information($"No applicable venture found for itemId {queuedItem.ItemId}"); _pluginLog.Debug($"No applicable venture found for itemId {itemId} as {retainer.Job}");
return (null, null); return (null, null);
} }
var itemToGather = _gameCache.ItemsToGather.FirstOrDefault(x => x.ItemId == queuedItem.ItemId); var itemToGather = _gameCache.ItemsToGather.FirstOrDefault(x => x.ItemId == itemId);
if (itemToGather != null && !character.GatheredItems.Contains(itemToGather.GatheredItemId)) if (itemToGather != null && !character.GatheredItems.Contains(itemToGather.GatheredItemId))
{ {
_pluginLog.Information($"Character hasn't gathered {venture.Name} yet"); _pluginLog.Information($"Character hasn't gathered {venture.Name} yet");
return (null, null); return (null, null);
} }
_pluginLog.Information( _pluginLog.Debug(
$"Found venture {venture.Name}, row = {venture.RowId}, checking if it is suitable"); $"Found venture {venture.Name}, row = {venture.RowId}, checking if we have high enough stats");
VentureReward? reward = null; VentureReward? reward = null;
if (venture.CategoryName is "MIN" or "BTN") if (venture.CategoryName is "MIN" or "BTN")
{ {

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; }
}
}

View File

@ -1,426 +1,269 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Numerics; using System.Numerics;
using ARControl.External;
using ARControl.GameData; using ARControl.GameData;
using ARControl.Windows.Config;
using Dalamud.Game.Text;
using Dalamud.Interface; using Dalamud.Interface;
using Dalamud.Interface.Colors; using Dalamud.Interface.Colors;
using Dalamud.Interface.Components; using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing; using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin; using Dalamud.Plugin;
using Dalamud.Plugin.Services; using Dalamud.Plugin.Services;
using ECommons.ImGuiMethods; using ECommons;
using ImGuiNET; using ImGuiNET;
using LLib;
using LLib.ImGui;
namespace ARControl.Windows; namespace ARControl.Windows;
internal sealed class ConfigWindow : Window internal sealed class ConfigWindow : LWindow
{ {
private const byte MaxLevel = 90; public const byte MinLevel = 10;
private static readonly Vector4 ColorGreen = ImGuiColors.HealerGreen; private readonly IDalamudPluginInterface _pluginInterface;
private static readonly Vector4 ColorRed = ImGuiColors.DalamudRed;
private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey;
private readonly DalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration; private readonly Configuration _configuration;
private readonly GameCache _gameCache; private readonly GameCache _gameCache;
private readonly IClientState _clientState;
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog; private readonly IPluginLog _pluginLog;
private string _searchString = string.Empty; private readonly List<ITab> _tabs;
private Configuration.QueuedItem? _dragDropSource;
private bool _enableDragDrop; private bool _shouldSave;
private bool _checkPerCharacter = true; private (string, int)? _draggedItem;
private bool _onlyShowMissing = true;
public ConfigWindow( public ConfigWindow(
DalamudPluginInterface pluginInterface, IDalamudPluginInterface pluginInterface,
Configuration configuration, Configuration configuration,
GameCache gameCache, GameCache gameCache,
IClientState clientState, IClientState clientState,
ICommandManager commandManager, ICommandManager commandManager,
IconCache iconCache,
DiscardHelperIpc discardHelperIpc,
AllaganToolsIpc allaganToolsIpc,
IPluginLog pluginLog) IPluginLog pluginLog)
: base("ARC###ARControlConfig") : base($"ARC {SeIconChar.Collectible.ToIconString()}###ARControlConfig")
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_configuration = configuration; _configuration = configuration;
_gameCache = gameCache; _gameCache = gameCache;
_clientState = clientState;
_commandManager = commandManager;
_pluginLog = pluginLog; _pluginLog = pluginLog;
_tabs =
[
new VentureListTab(this, _configuration, gameCache, iconCache, discardHelperIpc, pluginLog),
new CharacterGroupTab(this, _configuration),
new RetainersTab(this, _configuration, iconCache),
new InventoryTab(_configuration, allaganToolsIpc, _gameCache, pluginLog),
new LockedItemsTab(this, _configuration, clientState, commandManager, gameCache),
new MiscTab(this, _configuration),
];
SizeConstraints = new()
{
MinimumSize = new Vector2(480, 300),
MaximumSize = new Vector2(9999, 9999),
};
} }
public float MainIndentSize { get; private set; } = 1;
public override void Draw() public override void Draw()
{ {
if (ImGui.BeginTabBar("ARConfigTabs")) using var tabBar = ImRaii.TabBar("ARConfigTabs");
{ if (!tabBar)
DrawItemQueue(); return;
DrawCharacters();
DrawGatheredItemsToCheck();
ImGui.EndTabBar();
}
}
private unsafe void DrawItemQueue() ImGui.PushFont(UiBuilder.IconFont);
{ MainIndentSize = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X +
if (ImGui.BeginTabItem("Venture Queue")) ImGui.GetStyle().FramePadding.X * 2f +
{ ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().WindowPadding.X / 2;
if (ImGui.BeginCombo("Add Item...##VentureSelection", "")) ImGui.PopFont();
{
ImGuiEx.SetNextItemFullWidth();
ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256);
foreach (var ventures in _gameCache.Ventures foreach (var tab in _tabs)
.Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) tab.Draw();
.OrderBy(x => x.Level)
.ThenBy(x => x.Name) if (_shouldSave && !ImGui.IsAnyMouseDown())
.ThenBy(x => x.ItemId)
.GroupBy(x => x.ItemId))
{ {
var venture = ventures.First(); _pluginLog.Debug("Triggering delayed save");
if (ImGui.Selectable(
$"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})##SelectVenture{venture.RowId}"))
{
_configuration.QueuedItems.Add(new Configuration.QueuedItem
{
ItemId = venture.ItemId,
RemainingQuantity = 0,
});
_searchString = string.Empty;
Save(); Save();
} }
} }
ImGui.EndCombo(); internal void DrawVentureListSelection(string id, List<Guid> selectedLists)
{
ImGui.PushID($"##ListSelection{id}");
List<(Guid Id, string Name, Configuration.ItemList List)> itemLists = new List<Configuration.ItemList>
{
new()
{
Id = Guid.Empty,
Name = "---",
Type = Configuration.ListType.CollectOneTime,
Priority = Configuration.ListPriority.InOrder,
CheckRetainerInventory = false,
} }
}.Concat(_configuration.ItemLists)
ImGui.Separator(); .Select(x => (x.Id, $"{x.Name} {x.GetIcon()}".TrimEnd(), x)).ToList();
int? itemToRemove = null;
ImGui.Indent(30); int? itemToAdd = null;
Configuration.QueuedItem? itemToRemove = null;
Configuration.QueuedItem? itemToAdd = null;
int indexToAdd = 0; int indexToAdd = 0;
for (int i = 0; i < _configuration.QueuedItems.Count; ++i)
{
var item = _configuration.QueuedItems[i];
ImGui.PushID($"QueueItem{i}");
var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList();
var venture = ventures.First();
if (!_enableDragDrop) float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < selectedLists.Count; ++i)
{ {
ImGui.SetNextItemWidth(130); Vector2 topLeft = ImGui.GetCursorScreenPos() +
int quantity = item.RemainingQuantity; new Vector2(-MainIndentSize, -ImGui.GetStyle().ItemSpacing.Y / 2);
if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})",
ref quantity, 100)) ImGui.PushID($"##{id}_Item{i}");
var listId = selectedLists[i];
var listIndex = itemLists.FindIndex(x => x.Id == listId);
ImGui.PushFont(UiBuilder.IconFont);
ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X -
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X -
ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X -
ImGui.GetStyle().FramePadding.X * 4 -
ImGui.GetStyle().ItemSpacing.X * 2);
ImGui.PopFont();
if (ImGui.Combo("", ref listIndex, itemLists.Select(x => x.Name).ToArray(), itemLists.Count))
{ {
item.RemainingQuantity = quantity; selectedLists[i] = itemLists[listIndex].Id;
Save(); Save();
} }
if (selectedLists.Count > 1)
{
ImGui.SameLine();
if (_draggedItem != null && _draggedItem.Value.Item1 == id && _draggedItem.Value.Item2 == i)
{
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 = (id, i);
ImGui.SameLine();
} }
else else
{ {
ImGui.Selectable($"{item.RemainingQuantity}x {venture.Name}"); ImGui.PushFont(UiBuilder.IconFont);
ImGui.SameLine(0,
if (ImGui.BeginDragDropSource()) ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X +
{ ImGui.GetStyle().FramePadding.X * 2 + ImGui.GetStyle().ItemSpacing.X * 2);
ImGui.SetDragDropPayload("ArcDragDrop", nint.Zero, 0); ImGui.PopFont();
_dragDropSource = item;
ImGui.EndDragDropSource();
} }
if (ImGui.BeginDragDropTarget()) if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
{ itemToRemove = i;
if (_dragDropSource != null && ImGui.AcceptDragDropPayload("ArcDragDrop").NativePtr != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = null; if (listIndex > 0)
{
if (selectedLists.Take(i).Any(x => x == listId))
{
ImGui.Indent(MainIndentSize);
ImGui.TextColored(ImGuiColors.DalamudYellow, "This entry is a duplicate and will be ignored.");
ImGui.Unindent(MainIndentSize);
}
else if (_configuration.ConfigUiOptions.ShowVentureListContents)
{
var list = itemLists[listIndex].List;
ImGui.Indent(MainIndentSize);
ImGui.Text(list.Type == Configuration.ListType.CollectOneTime
? "Items on this list will be collected once."
: "Items on this list will be kept in stock on each character.");
ImGui.Spacing();
foreach (var item in list.Items)
{
var venture = _gameCache.Ventures.First(x => x.ItemId == item.ItemId);
ImGui.Text($"{item.RemainingQuantity}x {venture.Name}");
} }
ImGui.EndDragDropTarget(); ImGui.Unindent(MainIndentSize);
} }
} }
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopupContextItem($"###ctx{i}"))
{
if (ImGui.MenuItem($"Remove {venture.Name}"))
itemToRemove = item;
ImGui.EndPopup();
}
ImGui.PopID(); ImGui.PopID();
Vector2 bottomRight = new Vector2(topLeft.X + width + 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 == id)
{
int oldIndex = _draggedItem.Value.Item2;
var draggedItem = selectedLists[oldIndex];
var (topLeft, bottomRight) = itemPositions[oldIndex];
topLeft += new Vector2(MainIndentSize, 0);
ImGui.GetWindowDrawList().AddRect(topLeft, bottomRight, ImGui.GetColorU32(ImGuiColors.DalamudGrey), 3f,
ImDrawFlags.RoundCornersAll);
int newIndex = itemPositions.IndexOf(x => ImGui.IsMouseHoveringRect(x.TopLeft, x.BottomRight, true));
if (newIndex >= 0 && oldIndex != newIndex)
{
itemToAdd = _draggedItem.Value.Item2;
indexToAdd = newIndex;
_draggedItem = (_draggedItem.Value.Item1, newIndex);
}
} }
if (itemToRemove != null) if (itemToRemove != null)
{ {
_configuration.QueuedItems.Remove(itemToRemove); selectedLists.RemoveAt(itemToRemove.Value);
Save(); Save();
} }
if (itemToAdd != null) if (itemToAdd != null)
{ {
_pluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}"); Guid listId = selectedLists[itemToAdd.Value];
_configuration.QueuedItems.Remove(itemToAdd); selectedLists.RemoveAt(itemToAdd.Value);
_configuration.QueuedItems.Insert(indexToAdd, itemToAdd); selectedLists.Insert(indexToAdd, listId);
Save(); Save();
} }
ImGui.Unindent(30); var unusedLists = itemLists.Where(x => x.Id != Guid.Empty && !selectedLists.Contains(x.Id)).ToList();
ImGui.BeginDisabled(unusedLists.Count == 0);
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Venture List to this Group"))
ImGui.OpenPopup($"##AddItem{id}");
if (_configuration.QueuedItems.Count > 0) if (ImGui.BeginPopupContextItem($"##AddItem{id}", ImGuiPopupFlags.NoOpenOverItems))
ImGui.Separator();
if (ImGuiComponents.IconButtonWithText(_enableDragDrop ? FontAwesomeIcon.Times : FontAwesomeIcon.Sort, _enableDragDrop ? "Disable Drag&Drop" : "Enable Drag&Drop"))
{ {
_enableDragDrop = !_enableDragDrop; foreach (var list in unusedLists)
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove all finished items"))
{ {
if (_configuration.QueuedItems.RemoveAll(q => q.RemainingQuantity == 0) > 0) if (ImGui.MenuItem($"{list.Name}##{list.Id}"))
{
selectedLists.Add(list.Id);
Save(); Save();
} }
ImGui.EndTabItem();
}
} }
private void DrawCharacters() ImGui.EndPopup();
{
if (ImGui.BeginTabItem("Retainers"))
{
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.PushItemWidth(ImGui.GetFontSize() * 30);
Vector4 buttonColor = new Vector4();
if (character is { Managed: true, 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))
{
character.Managed = !character.Managed;
Save();
}
ImGui.SameLine();
if (ImGui.CollapsingHeader(
$"{character.CharacterName} {(character.Managed ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}"))
{
ImGui.Indent(30);
foreach (var retainer in character.Retainers.Where(x => x.Job > 0).OrderBy(x => x.DisplayOrder))
{
ImGui.BeginDisabled(retainer.Level < MaxLevel);
bool managed = retainer.Managed && retainer.Level == MaxLevel;
ImGui.Text(_gameCache.Jobs[retainer.Job]);
ImGui.SameLine();
if (ImGui.Checkbox($"{retainer.Name}###Retainer{retainer.Name}{retainer.DisplayOrder}",
ref managed))
{
retainer.Managed = managed;
Save();
} }
ImGui.EndDisabled(); ImGui.EndDisabled();
}
ImGui.Unindent(30);
}
ImGui.PopID(); ImGui.PopID();
} }
}
ImGui.EndTabItem();
}
}
private void DrawGatheredItemsToCheck()
{
if (ImGui.BeginTabItem("Locked Items"))
{
ImGui.Checkbox("Group by character", ref _checkPerCharacter);
ImGui.Checkbox("Only show missing items", ref _onlyShowMissing);
ImGui.Separator();
var itemsToCheck =
_configuration.QueuedItems
.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.Managed)
.OrderBy(x => x.WorldName)
.ThenBy(x => x.LocalContentId)
.Select(x => new CheckedCharacter(x, itemsToCheck))
.ToList();
if (_checkPerCharacter)
{
foreach (var ch in charactersToCheck.Where(x => x.ToCheck(_onlyShowMissing).Any()))
{
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(30);
foreach (var item in itemsToCheck.Where(x =>
ch.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId)))
{
var color = ch.Items[item.ItemId];
if (color != ColorGrey)
{
ImGui.PushStyleColor(ImGuiCol.Text, color);
if (currentCharacter && color == ColorRed)
{
ImGui.Selectable(item.GatheredItem.Name);
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(item.GatheredItem.Name);
}
ImGui.PopStyleColor();
}
}
ImGui.Unindent(30);
}
}
}
else
{
foreach (var item in itemsToCheck.Where(x =>
charactersToCheck.Any(y => y.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId))))
{
if (ImGui.CollapsingHeader($"{item.GatheredItem.Name}##Gathered{item.GatheredItem.ItemId}"))
{
ImGui.Indent(30);
foreach (var ch in charactersToCheck)
{
var color = ch.Items[item.ItemId];
if (color == ColorRed || (color == ColorGreen && !_onlyShowMissing))
ImGui.TextColored(color, ch.Character.ToString());
}
ImGui.Unindent(30);
}
}
}
ImGui.EndTabItem();
}
}
private void Save() private void Save()
{ {
_pluginInterface.SavePluginConfig(_configuration); _pluginInterface.SavePluginConfig(_configuration);
_shouldSave = false;
} }
private sealed class CheckedCharacter public void ShouldSave() => _shouldSave = true;
{
public CheckedCharacter(Configuration.CharacterConfiguration character,
List<CheckedItem> itemsToCheck)
{
Character = character;
foreach (var item in itemsToCheck)
{
bool enabled = character.Retainers.Any(x => item.Ventures.Any(v => v.MatchesJob(x.Job)));
if (enabled)
{
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

@ -1,12 +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": {
"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

1
LLib Submodule

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