Compare commits

..

48 Commits
v1.0 ... master

Author SHA1 Message Date
Liza e834160cd3
Show last updated time on Inventory tab 2024-08-02 15:51:49 +02:00
Liza 9250044c60
Retainers can now be disabled if they're under the minimum level (but not enabled) 2024-07-28 17:58:45 +02:00
Liza 8f6b38a3e8
Add inventory tab 2024-07-23 17:07:25 +02:00
Liza 9efb28cbb0
Add a tooltip for certain shard/crystal configurations 2024-07-22 10:38:10 +02:00
Liza 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
Liza c30abb79ab
Fix button alignment when no drag/drop button is visible 2024-07-19 19:23:03 +02:00
Liza 89951227af
Support drag & drop to redorder lists 2024-07-18 19:38:43 +02:00
Liza 44f44bce8a
Split UI into different classes 2024-07-18 18:36:56 +02:00
Liza c522574ab9
Update venture list item sorting 2024-07-18 18:00:42 +02:00
Liza ec56f22e36
Don't attempt to clear clipboard text after paste 2024-07-18 16:29:32 +02:00
Liza 9ecbd1e3b2
API 10 2024-07-03 19:08:40 +02:00
Liza 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
Liza 8a3a6399b2
Make assignment chat message optional 2024-04-26 20:02:23 +02:00
Liza b66b4689b8
Improve icon size calculations 2024-03-27 18:35:40 +01:00
Liza bc65bfe24c
Fix scaling/spacing issues 2024-03-27 09:30:17 +01:00
Liza 238b58d555
NET 8 2024-03-27 08:26:14 +01:00
Liza ec256ac090
Add 'Check Retainer Inventory' for lists to keep in stock 2024-01-24 16:25:13 +01:00
Liza e78cd642cd
Fix issue where non-existent retainers could cause plugin to not work properly 2024-01-20 02:38:50 +01:00
Liza 37e9f29d41
Show warning icon for icons that are on auto-discard list 2024-01-13 23:36:24 +01:00
Liza a722f6a303
Highlight current player when viewing 'Locked items', but grouped by venture instead of character 2024-01-07 18:54:36 +01:00
Liza 31cbb118a4
Use RetainerId instead of Name as id trait 2024-01-07 18:31:32 +01:00
Liza 9477be08d7
Don't fetch more items when stocking up if at the exact item count 2023-11-20 12:33:13 +01:00
Liza 75885e004a
Bump version/recompile 2023-11-14 20:13:57 +01:00
Liza 1002e85eb4
Update header icon logic for Dalamud changes 2023-11-09 11:40:46 +01:00
Liza ba40a916b3
'Add venture' filter now adds the first venture when pressing enter 2023-11-04 01:47:20 +01:00
Liza cfc41a5a61
Import from clipboard, short notation for adding new items 2023-11-02 02:28:26 +01:00
Liza 3138d6fc5f
Use AutoRetainerAPI as submodule 2023-11-01 10:26:40 +01:00
Liza 6b432bfc77
Move IconCache to LLib 2023-11-01 10:24:49 +01:00
Liza 8b11b67c96
Add a configurable minimum venture count, under which only quick ventures are assigned 2023-10-19 01:22:57 +02:00
Liza 65af6814df
Add UI/sorting options 2023-10-17 12:51:06 +02:00
Liza 297544128d
Set missing popup flag 2023-10-17 12:12:44 +02:00
Liza 26d8a005ba
Update submodule URL 2023-10-14 00:00:31 +02:00
Liza 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
Liza 37bdefcc55
Change log levels 2023-10-12 11:07:52 +02:00
Liza 2cd9013e3e
Bump Version for repo 2023-10-12 02:13:58 +02:00
Liza 7676e3b2b1
Bump AR 2023-10-12 01:41:22 +02:00
Liza c7b5a7bee2
Merge branch 'venture-2.x' 2023-10-12 01:40:36 +02:00
Liza 2f65d7c951
Replace overlay icon 2023-10-11 10:42:20 +02:00
Liza fbba9244a6
(2.x) Improve chat feedback 2023-10-11 10:39:43 +02:00
Liza fe29867929
Add LLib 2023-10-11 10:19:58 +02:00
Liza 7332f9945f
Change the minimum level for retainers to work 2023-10-10 16:29:44 +02:00
Liza d8cfb75c33
(2.x) Pick appropriate venture for new list types/priorities 2023-10-10 16:27:10 +02:00
Liza 59d6f50c18
(2.x) Add icons 2023-10-10 13:26:25 +02:00
Liza 6564a49369
(2.x) Update Lists/locked items tabs 2023-10-10 12:08:26 +02:00
Liza 189d4fb0a7
(2.x) Update Retainer/Group config tabs 2023-10-09 01:48:27 +02:00
Liza 2f8fb54e70
Update Icon 2023-10-05 16:53:29 +02:00
Liza 6645241651
Clean up dependencies 2023-10-04 12:32:11 +02:00
28 changed files with 3691 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
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARControl.csproj", "{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}"
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
GlobalSection(SolutionConfigurationPlatforms) = preSolution
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}.Release|Any CPU.ActiveCfg = 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
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>
<TargetFramework>net7.0-windows</TargetFramework>
<Version>1.0</Version>
<LangVersion>11.0</LangVersion>
<Nullable>enable</Nullable>
<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<Version>5.7</Version>
<OutputPath>dist</OutputPath>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<DebugType>portable</DebugType>
<PathMap Condition="$(SolutionDir) != ''">$(SolutionDir)=X:\</PathMap>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<PropertyGroup>
<DalamudLibPath>$(appdata)\XIVLauncher\addon\Hooks\dev\</DalamudLibPath>
<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>
<Import Project="..\LLib\LLib.targets"/>
<Import Project="..\LLib\RenameZip.targets"/>
<ItemGroup>
<PackageReference Include="DalamudPackager" Version="2.1.12"/>
<ProjectReference Include="..\AutoRetainerAPI\AutoRetainerAPI\AutoRetainerAPI.csproj" />
<ProjectReference Include="..\LLib\LLib.csproj"/>
</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>

View File

@ -3,5 +3,6 @@
"Author": "Liza Carvelli",
"Punchline": "Better AutoRetainer Venture Planner",
"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;
@ -24,7 +25,7 @@ partial class AutoRetainerControlPlugin
LocalContentId = registeredCharacterId,
CharacterName = offlineCharacterData.Name,
WorldName = offlineCharacterData.World,
Managed = false,
Type = Configuration.CharacterType.NotManaged,
};
save = true;
@ -37,13 +38,53 @@ partial class AutoRetainerControlPlugin
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;
}
// 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)
{
retainer = new Configuration.RetainerConfiguration
{
RetainerContentId = retainerData.RetainerID,
Name = retainerData.Name,
Managed = false,
};
@ -52,6 +93,12 @@ partial class AutoRetainerControlPlugin
character.Retainers.Add(retainer);
}
if (retainer.Name != retainerData.Name)
{
retainer.Name = retainerData.Name;
save = true;
}
if (retainer.DisplayOrder != retainerData.DisplayOrder)
{
retainer.DisplayOrder = retainerData.DisplayOrder;
@ -70,6 +117,12 @@ partial class AutoRetainerControlPlugin
save = true;
}
if (retainer.HasVenture != retainerData.HasVenture)
{
retainer.HasVenture = retainerData.HasVenture;
save = true;
}
if (retainer.LastVenture != retainerData.VentureID)
{
retainer.LastVenture = retainerData.VentureID;
@ -96,6 +149,17 @@ partial class AutoRetainerControlPlugin
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)

View File

@ -1,26 +1,32 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using ARControl.External;
using ARControl.GameData;
using ARControl.Windows;
using AutoRetainerAPI;
using Dalamud.Game.Command;
using Dalamud.Interface;
using Dalamud.Interface.Components;
using Dalamud.Game.Text;
using Dalamud.Game.Text.SeStringHandling;
using Dalamud.Game.Text.SeStringHandling.Payloads;
using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET;
using LLib;
namespace ARControl;
[SuppressMessage("ReSharper", "UnusedType.Global")]
public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{
private const int QuickVentureId = 395;
private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin));
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
private readonly IChatGui _chatGui;
private readonly ICommandManager _commandManager;
@ -28,28 +34,41 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private readonly Configuration _configuration;
private readonly GameCache _gameCache;
private readonly IconCache _iconCache;
private readonly VentureResolver _ventureResolver;
private readonly AllaganToolsIpc _allaganToolsIpc;
private readonly ConfigWindow _configWindow;
private readonly AutoRetainerApi _autoRetainerApi;
private readonly AutoRetainerReflection _autoRetainerReflection;
public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, IDataManager dataManager,
IClientState clientState, IChatGui chatGui, ICommandManager commandManager, IPluginLog pluginLog)
public AutoRetainerControlPlugin(IDalamudPluginInterface pluginInterface, IDataManager dataManager,
IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider,
IFramework framework, IPluginLog pluginLog)
{
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface;
_clientState = clientState;
_chatGui = chatGui;
_commandManager = commandManager;
_pluginLog = pluginLog;
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration();
_configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration { Version = 2 };
_gameCache = new GameCache(dataManager);
_iconCache = new IconCache(textureProvider);
_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);
ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new();
_autoRetainerReflection = new AutoRetainerReflection(pluginInterface, framework, pluginLog, _autoRetainerApi);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
@ -62,81 +81,295 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
});
if (_autoRetainerApi.Ready)
Sync();
{
try
{
Sync();
}
catch (Exception e)
{
_pluginLog.Error(e, "Unable to sync characters");
_chatGui.PrintError(
"Unable to synchronize characters with AutoRetainer, plugin might not work properly.");
}
}
}
private void SendRetainerToVenture(string retainerName)
{
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);
if (ch == null)
{
_pluginLog.Information("No character information found");
return null;
}
else if (!ch.Managed)
if (ch.Type == Configuration.CharacterType.NotManaged)
{
_pluginLog.Information("Character is not managed");
return null;
}
var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName);
if (retainer == null)
{
_pluginLog.Information("No retainer information found");
return null;
}
if (!retainer.Managed)
{
_pluginLog.Information("Retainer is not managed");
return null;
}
_pluginLog.Information("Checking tasks...");
Sync();
if (ch.Ventures == 0)
{
_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
{
var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName);
if (retainer == null)
var venturesInProgress = CalculateVenturesInProgress(ch);
foreach (var inProgress in venturesInProgress)
{
_pluginLog.Information("No retainer information found");
}
else if (!retainer.Managed)
{
_pluginLog.Information("Retainer is not managed");
_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
{
_pluginLog.Information("Checking tasks...");
Sync();
foreach (var queuedItem in _configuration.QueuedItems.Where(x => x.RemainingQuantity > 0))
var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId);
if (group == null)
{
_pluginLog.Information($"Checking venture info for itemId {queuedItem.ItemId}");
_pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}.");
return null;
}
var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, queuedItem);
if (reward == 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)
{
_pluginLog.Information("Retainer can't complete venture");
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
{
_chatGui.Print(
$"[ARC] Sending retainer {retainerName} to collect {reward.Quantity}x {venture!.Name}.");
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
_autoRetainerApi.SetVenture(venture.RowId);
if (_configuration.ConfigUiOptions.ShowAssignmentChatMessages || dryRun)
PrintNextVentureMessage(retainerName, venture, reward, list);
retainer.LastVenture = venture.RowId;
queuedItem.RemainingQuantity =
Math.Max(0, queuedItem.RemainingQuantity - reward.Quantity);
_pluginInterface.SavePluginConfig(_configuration);
return;
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 != 395)
{
_chatGui.Print($"[ARC] No tasks left for retainer {retainerName}, sending to Quick Venture.");
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QC");
_autoRetainerApi.SetVenture(395);
retainer.LastVenture = 395;
_pluginInterface.SavePluginConfig(_configuration);
}
else
_pluginLog.Information("Not changing venture plan, already 395");
}
}
// 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(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" Sending retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(" to collect ")
.Append(new UIForegroundPayload(1))
.Append($"{reward.Quantity}x ")
.Append(new ItemPayload(venture.ItemId))
.Append(venture.Name)
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" for ")
.Append(new UIForegroundPayload(1))
.Append($"{list.Name} {list.GetIcon()}")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
}
private void PrintEndOfListMessage(string retainerName, Configuration.RetainerConfiguration retainer)
{
_chatGui.Print(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" No tasks left for retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(", sending to ")
.Append(new UIForegroundPayload(1))
.Append("Quick Venture")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QV");
}
/// <remarks>
/// 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
inProgress[itemId] = reward.Quantity;
}
}
return inProgress;
}
private void RetainerTaskButtonDraw(ulong characterId, string retainerName)
{
Configuration.CharacterConfiguration? characterConfiguration =
_configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId);
if (characterConfiguration is not { Managed: true })
if (characterConfiguration is not { Type: not Configuration.CharacterType.NotManaged })
return;
Configuration.RetainerConfiguration? retainer =
@ -145,7 +378,19 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
return;
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();
@ -154,6 +399,43 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{
if (arguments == "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
_configWindow.Toggle();
}
@ -167,7 +449,22 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_iconCache.Dispose();
_autoRetainerReflection.Dispose();
_autoRetainerApi.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,27 +1,83 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Dalamud.Configuration;
using Dalamud.Game.Text;
using Newtonsoft.Json;
namespace ARControl;
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<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
{
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
public required uint ItemId { 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 required ulong LocalContentId { get; set; }
public required string CharacterName { 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 HashSet<uint> GatheredItems { get; set; } = new();
@ -29,16 +85,46 @@ internal sealed class Configuration : IPluginConfiguration
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 ulong RetainerContentId { get; set; }
public required string Name { get; set; }
public required bool Managed { get; set; }
public int DisplayOrder { get; set; }
public int Level { get; set; }
public uint Job { get; set; }
public bool HasVenture { get; set; }
public uint LastVenture { get; set; }
public int ItemLevel { get; set; }
public int Gathering { 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)"
MakeZip="true"
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>
</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

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

View File

@ -15,26 +15,26 @@ internal sealed class VentureResolver
}
public (Venture?, VentureReward?) ResolveVenture(Configuration.CharacterConfiguration character,
Configuration.RetainerConfiguration retainer, Configuration.QueuedItem queuedItem)
Configuration.RetainerConfiguration retainer, uint itemId)
{
var venture = _gameCache.Ventures
.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)
{
_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);
}
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))
{
_pluginLog.Information($"Character hasn't gathered {venture.Name} yet");
return (null, null);
}
_pluginLog.Information(
$"Found venture {venture.Name}, row = {venture.RowId}, checking if it is suitable");
_pluginLog.Debug(
$"Found venture {venture.Name}, row = {venture.RowId}, checking if we have high enough stats");
VentureReward? reward = null;
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,253 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
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 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)
{
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(_configWindow.MainIndentSize + ImGui.GetStyle().FramePadding.X);
}
}
}
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(_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();
}
ImGui.PushStyleColor(ImGuiCol.Text, color);
ImGui.TextUnformatted(ch.Character.ToString());
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.IndexOf(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.Numerics;
using ARControl.External;
using ARControl.GameData;
using ARControl.Windows.Config;
using Dalamud.Game.Text;
using Dalamud.Interface;
using Dalamud.Interface.Colors;
using Dalamud.Interface.Components;
using Dalamud.Interface.Windowing;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons.ImGuiMethods;
using ECommons;
using ImGuiNET;
using LLib;
using LLib.ImGui;
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 static readonly Vector4 ColorRed = ImGuiColors.DalamudRed;
private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey;
private readonly DalamudPluginInterface _pluginInterface;
private readonly IDalamudPluginInterface _pluginInterface;
private readonly Configuration _configuration;
private readonly GameCache _gameCache;
private readonly IClientState _clientState;
private readonly ICommandManager _commandManager;
private readonly IPluginLog _pluginLog;
private string _searchString = string.Empty;
private Configuration.QueuedItem? _dragDropSource;
private bool _enableDragDrop;
private bool _checkPerCharacter = true;
private bool _onlyShowMissing = true;
private readonly List<ITab> _tabs;
private bool _shouldSave;
private (string, int)? _draggedItem;
public ConfigWindow(
DalamudPluginInterface pluginInterface,
IDalamudPluginInterface pluginInterface,
Configuration configuration,
GameCache gameCache,
IClientState clientState,
ICommandManager commandManager,
IconCache iconCache,
DiscardHelperIpc discardHelperIpc,
AllaganToolsIpc allaganToolsIpc,
IPluginLog pluginLog)
: base("ARC###ARControlConfig")
: base($"ARC {SeIconChar.Collectible.ToIconString()}###ARControlConfig")
{
_pluginInterface = pluginInterface;
_configuration = configuration;
_gameCache = gameCache;
_clientState = clientState;
_commandManager = commandManager;
_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()
{
if (ImGui.BeginTabBar("ARConfigTabs"))
using var tabBar = ImRaii.TabBar("ARConfigTabs");
if (!tabBar)
return;
ImGui.PushFont(UiBuilder.IconFont);
MainIndentSize = ImGui.CalcTextSize(FontAwesomeIcon.Cog.ToIconString()).X +
ImGui.GetStyle().FramePadding.X * 2f +
ImGui.GetStyle().ItemSpacing.X - ImGui.GetStyle().WindowPadding.X / 2;
ImGui.PopFont();
foreach (var tab in _tabs)
tab.Draw();
if (_shouldSave && !ImGui.IsAnyMouseDown())
{
DrawItemQueue();
DrawCharacters();
DrawGatheredItemsToCheck();
ImGui.EndTabBar();
_pluginLog.Debug("Triggering delayed save");
Save();
}
}
private unsafe void DrawItemQueue()
internal void DrawVentureListSelection(string id, List<Guid> selectedLists)
{
if (ImGui.BeginTabItem("Venture Queue"))
{
if (ImGui.BeginCombo("Add Item...##VentureSelection", ""))
ImGui.PushID($"##ListSelection{id}");
List<(Guid Id, string Name, Configuration.ItemList List)> itemLists = new List<Configuration.ItemList>
{
ImGuiEx.SetNextItemFullWidth();
ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256);
foreach (var ventures in _gameCache.Ventures
.Where(x => x.Name.ToLower().Contains(_searchString.ToLower()))
.OrderBy(x => x.Level)
.ThenBy(x => x.Name)
.ThenBy(x => x.ItemId)
.GroupBy(x => x.ItemId))
new()
{
var venture = ventures.First();
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();
}
Id = Guid.Empty,
Name = "---",
Type = Configuration.ListType.CollectOneTime,
Priority = Configuration.ListPriority.InOrder,
CheckRetainerInventory = false,
}
}.Concat(_configuration.ItemLists)
.Select(x => (x.Id, $"{x.Name} {x.GetIcon()}".TrimEnd(), x)).ToList();
int? itemToRemove = null;
int? itemToAdd = null;
int indexToAdd = 0;
ImGui.EndCombo();
float width = ImGui.GetContentRegionAvail().X;
List<(Vector2 TopLeft, Vector2 BottomRight)> itemPositions = [];
for (int i = 0; i < selectedLists.Count; ++i)
{
Vector2 topLeft = ImGui.GetCursorScreenPos() +
new Vector2(-MainIndentSize, -ImGui.GetStyle().ItemSpacing.Y / 2);
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))
{
selectedLists[i] = itemLists[listIndex].Id;
Save();
}
ImGui.Separator();
ImGui.Indent(30);
Configuration.QueuedItem? itemToRemove = null;
Configuration.QueuedItem? itemToAdd = null;
int indexToAdd = 0;
for (int i = 0; i < _configuration.QueuedItems.Count; ++i)
if (selectedLists.Count > 1)
{
var item = _configuration.QueuedItems[i];
ImGui.PushID($"QueueItem{i}");
var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList();
var venture = ventures.First();
ImGui.SameLine();
if (!_enableDragDrop)
if (_draggedItem != null && _draggedItem.Value.Item1 == id && _draggedItem.Value.Item2 == i)
{
ImGui.SetNextItemWidth(130);
int quantity = item.RemainingQuantity;
if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})",
ref quantity, 100))
{
item.RemainingQuantity = quantity;
Save();
}
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown,
ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive)));
}
else
{
ImGui.Selectable($"{item.RemainingQuantity}x {venture.Name}");
ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown);
if (ImGui.BeginDragDropSource())
{
ImGui.SetDragDropPayload("ArcDragDrop", nint.Zero, 0);
_dragDropSource = item;
if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left))
_draggedItem = (id, i);
ImGui.EndDragDropSource();
}
if (ImGui.BeginDragDropTarget())
{
if (_dragDropSource != null && ImGui.AcceptDragDropPayload("ArcDragDrop").NativePtr != null)
{
itemToAdd = _dragDropSource;
indexToAdd = i;
_dragDropSource = null;
}
ImGui.EndDragDropTarget();
}
}
ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight);
if (ImGui.BeginPopupContextItem($"###ctx{i}"))
{
if (ImGui.MenuItem($"Remove {venture.Name}"))
itemToRemove = item;
ImGui.EndPopup();
}
ImGui.PopID();
}
if (itemToRemove != null)
{
_configuration.QueuedItems.Remove(itemToRemove);
Save();
}
if (itemToAdd != null)
{
_pluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}");
_configuration.QueuedItems.Remove(itemToAdd);
_configuration.QueuedItems.Insert(indexToAdd, itemToAdd);
Save();
}
ImGui.Unindent(30);
if (_configuration.QueuedItems.Count > 0)
ImGui.Separator();
if (ImGuiComponents.IconButtonWithText(_enableDragDrop ? FontAwesomeIcon.Times : FontAwesomeIcon.Sort, _enableDragDrop ? "Disable Drag&Drop" : "Enable Drag&Drop"))
{
_enableDragDrop = !_enableDragDrop;
}
ImGui.SameLine();
if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove all finished items"))
{
if (_configuration.QueuedItems.RemoveAll(q => q.RemainingQuantity == 0) > 0)
Save();
}
ImGui.EndTabItem();
}
}
private void DrawCharacters()
{
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.Unindent(30);
}
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);
}
}
ImGui.SameLine();
}
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.PushFont(UiBuilder.IconFont);
ImGui.SameLine(0,
ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X +
ImGui.GetStyle().FramePadding.X * 2 + ImGui.GetStyle().ItemSpacing.X * 2);
ImGui.PopFont();
}
ImGui.Unindent(30);
if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times))
itemToRemove = i;
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.Unindent(MainIndentSize);
}
}
ImGui.EndTabItem();
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)
{
selectedLists.RemoveAt(itemToRemove.Value);
Save();
}
if (itemToAdd != null)
{
Guid listId = selectedLists[itemToAdd.Value];
selectedLists.RemoveAt(itemToAdd.Value);
selectedLists.Insert(indexToAdd, listId);
Save();
}
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 (ImGui.BeginPopupContextItem($"##AddItem{id}", ImGuiPopupFlags.NoOpenOverItems))
{
foreach (var list in unusedLists)
{
if (ImGui.MenuItem($"{list.Name}##{list.Id}"))
{
selectedLists.Add(list.Id);
Save();
}
}
ImGui.EndPopup();
}
ImGui.EndDisabled();
ImGui.PopID();
}
private void Save()
{
_pluginInterface.SavePluginConfig(_configuration);
_shouldSave = false;
}
private sealed class CheckedCharacter
{
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; }
}
public void ShouldSave() => _shouldSave = true;
}

View File

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