Compare commits

...

25 Commits
v2.8 ... 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
22 changed files with 3188 additions and 1225 deletions

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

View File

@ -44,14 +44,47 @@ partial class AutoRetainerControlPlugin
save = true; save = true;
} }
List<string> seenRetainers = new(); // remove retainers without name
foreach (var retainerData in offlineCharacterData.RetainerData) save |= character.Retainers.RemoveAll(x => string.IsNullOrEmpty(x.Name)) > 0;
// migrate legacy retainers
foreach (var legacyRetainer in character.Retainers.Where(x => x.RetainerContentId == 0))
{ {
var retainer = character.Retainers.SingleOrDefault(x => x.Name == retainerData.Name); _pluginLog.Information($"Migrating retainer '{legacyRetainer.Name}' (char: {character})");
var retainerData =
offlineCharacterData.RetainerData.SingleOrDefault(x => legacyRetainer.Name == x.Name);
if (retainerData != null)
{
_pluginLog.Information(
$"Assigning contentId {retainerData.RetainerID} to retainer {retainerData.Name}");
legacyRetainer.RetainerContentId = retainerData.RetainerID;
save = true;
}
}
var retainersWithoutContentId = character.Retainers.Where(c => c.RetainerContentId == 0).ToList();
if (retainersWithoutContentId.Count > 0)
{
foreach (var retainer in retainersWithoutContentId)
{
_pluginLog.Warning($"Removing retainer {retainer.Name} without contentId");
character.Retainers.Remove(retainer);
}
save = true;
}
List<ulong> unknownRetainerIds = offlineCharacterData.RetainerData.Select(x => x.RetainerID).Where(x => x != 0).ToList();
foreach (var retainerData in offlineCharacterData.RetainerData.Where(x => !string.IsNullOrEmpty(x.Name)))
{
unknownRetainerIds.Remove(retainerData.RetainerID);
var retainer = character.Retainers.SingleOrDefault(x => x.RetainerContentId == retainerData.RetainerID);
if (retainer == null) if (retainer == null)
{ {
retainer = new Configuration.RetainerConfiguration retainer = new Configuration.RetainerConfiguration
{ {
RetainerContentId = retainerData.RetainerID,
Name = retainerData.Name, Name = retainerData.Name,
Managed = false, Managed = false,
}; };
@ -60,7 +93,11 @@ partial class AutoRetainerControlPlugin
character.Retainers.Add(retainer); character.Retainers.Add(retainer);
} }
seenRetainers.Add(retainer.Name); if (retainer.Name != retainerData.Name)
{
retainer.Name = retainerData.Name;
save = true;
}
if (retainer.DisplayOrder != retainerData.DisplayOrder) if (retainer.DisplayOrder != retainerData.DisplayOrder)
{ {
@ -113,8 +150,16 @@ partial class AutoRetainerControlPlugin
} }
} }
if (character.Retainers.RemoveAll(x => !seenRetainers.Contains(x.Name)) > 0) if (unknownRetainerIds.Count > 0)
{
foreach (var retainerId in unknownRetainerIds)
{
_pluginLog.Warning($"Removing unknown retainer with contentId {retainerId}");
character.Retainers.RemoveAll(c => c.RetainerContentId == retainerId);
}
save = true; save = true;
}
} }
if (save) if (save)

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using ARControl.External;
using ARControl.GameData; using ARControl.GameData;
using ARControl.Windows; using ARControl.Windows;
using AutoRetainerAPI; using AutoRetainerAPI;
@ -25,7 +26,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private const int QuickVentureId = 395; private const int QuickVentureId = 395;
private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin)); private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin));
private readonly DalamudPluginInterface _pluginInterface; private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState; private readonly IClientState _clientState;
private readonly IChatGui _chatGui; private readonly IChatGui _chatGui;
private readonly ICommandManager _commandManager; private readonly ICommandManager _commandManager;
@ -35,13 +36,18 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private readonly GameCache _gameCache; private readonly GameCache _gameCache;
private readonly IconCache _iconCache; private readonly IconCache _iconCache;
private readonly VentureResolver _ventureResolver; private readonly VentureResolver _ventureResolver;
private readonly AllaganToolsIpc _allaganToolsIpc;
private readonly ConfigWindow _configWindow; private readonly ConfigWindow _configWindow;
private readonly AutoRetainerApi _autoRetainerApi; private readonly AutoRetainerApi _autoRetainerApi;
private readonly AutoRetainerReflection _autoRetainerReflection;
public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, IDataManager dataManager, public AutoRetainerControlPlugin(IDalamudPluginInterface pluginInterface, IDataManager dataManager,
IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider, IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider,
IPluginLog pluginLog) IFramework framework, IPluginLog pluginLog)
{ {
ArgumentNullException.ThrowIfNull(pluginInterface);
ArgumentNullException.ThrowIfNull(dataManager);
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_clientState = clientState; _clientState = clientState;
_chatGui = chatGui; _chatGui = chatGui;
@ -53,13 +59,16 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_gameCache = new GameCache(dataManager); _gameCache = new GameCache(dataManager);
_iconCache = new IconCache(textureProvider); _iconCache = new IconCache(textureProvider);
_ventureResolver = new VentureResolver(_gameCache, _pluginLog); _ventureResolver = new VentureResolver(_gameCache, _pluginLog);
DiscardHelperIpc discardHelperIpc = new(_pluginInterface);
_allaganToolsIpc = new AllaganToolsIpc(pluginInterface, pluginLog);
_configWindow = _configWindow =
new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache, new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache,
_pluginLog); discardHelperIpc, _allaganToolsIpc, _pluginLog);
_windowSystem.AddWindow(_configWindow); _windowSystem.AddWindow(_configWindow);
ECommonsMain.Init(_pluginInterface, this); ECommonsMain.Init(_pluginInterface, this);
_autoRetainerApi = new(); _autoRetainerApi = new();
_autoRetainerReflection = new AutoRetainerReflection(pluginInterface, framework, pluginLog, _autoRetainerApi);
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw; _pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle; _pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
@ -72,7 +81,18 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
}); });
if (_autoRetainerApi.Ready) if (_autoRetainerApi.Ready)
Sync(); {
try
{
Sync();
}
catch (Exception e)
{
_pluginLog.Error(e, "Unable to sync characters");
_chatGui.PrintError(
"Unable to synchronize characters with AutoRetainer, plugin might not work properly.");
}
}
} }
private void SendRetainerToVenture(string retainerName) private void SendRetainerToVenture(string retainerName)
@ -84,6 +104,12 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
private unsafe uint? GetNextVenture(string retainerName, bool dryRun) private unsafe uint? GetNextVenture(string retainerName, bool dryRun)
{ {
if (!_autoRetainerReflection.ShouldReassign)
{
_pluginLog.Information("AutoRetainer is configured to not reassign ventures, so we are not checking any venture lists.");
return null;
}
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId); var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null) if (ch == null)
{ {
@ -115,11 +141,13 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
if (ch.Ventures == 0) if (ch.Ventures == 0)
{ {
_pluginLog.Warning("Could not assign a next venture from venture list, as the character has no ventures left."); _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) 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."); _pluginLog.Warning(
$"Could not assign a next venture from venture list, character only has {ch.Ventures} left, configuration says to only send out above {_configuration.Misc.VenturesToKeep} ventures.");
} }
else else
{ {
@ -174,11 +202,12 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{ {
QueuedItem = x, QueuedItem = x,
InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) + InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) +
(venturesInProgress.TryGetValue(x.ItemId, out int inProgress) venturesInProgress.GetValueOrDefault(x.ItemId, 0) +
? inProgress (list.CheckRetainerInventory
? (int)_allaganToolsIpc.GetRetainerItemCount(x.ItemId)
: 0), : 0),
}) })
.Where(x => x.InventoryCount <= x.RequestedCount) .Where(x => x.InventoryCount < x.RequestedCount)
.ToList() .ToList()
.AsReadOnly(); .AsReadOnly();
@ -208,28 +237,8 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
} }
else else
{ {
_chatGui.Print( if (_configuration.ConfigUiOptions.ShowAssignmentChatMessages || dryRun)
new SeString(new UIForegroundPayload(579)) PrintNextVentureMessage(retainerName, venture, reward, list);
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" Sending retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(" to collect ")
.Append(new UIForegroundPayload(1))
.Append($"{reward.Quantity}x ")
.Append(new ItemPayload(venture.ItemId))
.Append(venture.Name)
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" for ")
.Append(new UIForegroundPayload(1))
.Append($"{list.Name} {list.GetIcon()}")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
if (!dryRun) if (!dryRun)
{ {
@ -254,21 +263,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
// fallback: managed but no venture found/ // fallback: managed but no venture found/
if (retainer.LastVenture != QuickVentureId) if (retainer.LastVenture != QuickVentureId)
{ {
_chatGui.Print( PrintEndOfListMessage(retainerName, retainer);
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" No tasks left for retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(", sending to ")
.Append(new UIForegroundPayload(1))
.Append("Quick Venture")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QV");
if (!dryRun) if (!dryRun)
{ {
retainer.HasVenture = true; retainer.HasVenture = true;
@ -292,6 +287,50 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
} }
} }
private void PrintNextVentureMessage(string retainerName, Venture venture, VentureReward reward, Configuration.ItemList list)
{
_chatGui.Print(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" Sending retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(" to collect ")
.Append(new UIForegroundPayload(1))
.Append($"{reward.Quantity}x ")
.Append(new ItemPayload(venture.ItemId))
.Append(venture.Name)
.Append(RawPayload.LinkTerminator)
.Append(new UIForegroundPayload(0))
.Append(" for ")
.Append(new UIForegroundPayload(1))
.Append($"{list.Name} {list.GetIcon()}")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information(
$"Setting AR to use venture {venture.RowId}, which should retrieve {reward.Quantity}x {venture.Name}");
}
private void PrintEndOfListMessage(string retainerName, Configuration.RetainerConfiguration retainer)
{
_chatGui.Print(
new SeString(new UIForegroundPayload(579))
.Append(SeIconChar.Collectible.ToIconString())
.Append(new UIForegroundPayload(0))
.Append($" No tasks left for retainer ")
.Append(new UIForegroundPayload(1))
.Append(retainerName)
.Append(new UIForegroundPayload(0))
.Append(", sending to ")
.Append(new UIForegroundPayload(1))
.Append("Quick Venture")
.Append(new UIForegroundPayload(0))
.Append("."));
_pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QV");
}
/// <remarks> /// <remarks>
/// This treats the retainer who is currently doing the venture as 'in-progress', since I believe the /// This treats the retainer who is currently doing the venture as 'in-progress', since I believe the
/// relevant event is fired BEFORE the venture rewards are collected. /// relevant event is fired BEFORE the venture rewards are collected.
@ -360,7 +399,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{ {
if (arguments == "sync") if (arguments == "sync")
Sync(); Sync();
else if (arguments.StartsWith("dnv")) else if (arguments.StartsWith("dnv", StringComparison.Ordinal))
{ {
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId); var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null || ch.Type == Configuration.CharacterType.NotManaged || ch.Retainers.Count == 0) if (ch == null || ch.Type == Configuration.CharacterType.NotManaged || ch.Retainers.Count == 0)
@ -374,7 +413,10 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
if (s.Length > 1) if (s.Length > 1)
retainerName = ch.Retainers.SingleOrDefault(x => x.Name.EqualsIgnoreCase(s[1]))?.Name; retainerName = ch.Retainers.SingleOrDefault(x => x.Name.EqualsIgnoreCase(s[1]))?.Name;
else else
retainerName = ch.Retainers.MinBy(x => x.DisplayOrder)?.Name; retainerName = ch.Retainers
.OrderBy(x => x.DisplayOrder)
.ThenBy(x => x.RetainerContentId)
.FirstOrDefault()?.Name;
if (retainerName == null) if (retainerName == null)
{ {
@ -408,6 +450,7 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw; _pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
_iconCache.Dispose(); _iconCache.Dispose();
_autoRetainerReflection.Dispose();
_autoRetainerApi.Dispose(); _autoRetainerApi.Dispose();
ECommonsMain.Dispose(); ECommonsMain.Dispose();
} }

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using Dalamud.Configuration; using Dalamud.Configuration;
using Dalamud.Game.Text; using Dalamud.Game.Text;
using Newtonsoft.Json;
namespace ARControl; namespace ARControl;
@ -21,6 +22,7 @@ internal sealed class Configuration : IPluginConfiguration
public required string Name { get; set; } public required string Name { get; set; }
public required ListType Type { get; set; } = ListType.CollectOneTime; public required ListType Type { get; set; } = ListType.CollectOneTime;
public required ListPriority Priority { get; set; } = ListPriority.InOrder; public required ListPriority Priority { get; set; } = ListPriority.InOrder;
public bool CheckRetainerInventory { get; set; }
public List<QueuedItem> Items { get; set; } = new(); public List<QueuedItem> Items { get; set; } = new();
public string GetIcon() public string GetIcon()
@ -52,6 +54,9 @@ internal sealed class Configuration : IPluginConfiguration
public sealed class QueuedItem public sealed class QueuedItem
{ {
[JsonIgnore]
public Guid InternalId { get; } = Guid.NewGuid();
public required uint ItemId { get; set; } public required uint ItemId { get; set; }
public required int RemainingQuantity { get; set; } public required int RemainingQuantity { get; set; }
} }
@ -97,6 +102,7 @@ internal sealed class Configuration : IPluginConfiguration
public sealed class RetainerConfiguration public sealed class RetainerConfiguration
{ {
public ulong RetainerContentId { get; set; }
public required string Name { get; set; } public required string Name { get; set; }
public required bool Managed { get; set; } public required bool Managed { get; set; }
public int DisplayOrder { get; set; } public int DisplayOrder { get; set; }
@ -119,6 +125,6 @@ internal sealed class Configuration : IPluginConfiguration
public bool ShowVentureListContents { get; set; } = true; public bool ShowVentureListContents { get; set; } = true;
public bool CheckGatheredItemsPerCharacter { get; set; } public bool CheckGatheredItemsPerCharacter { get; set; }
public bool OnlyShowMissingGatheredItems { get; set; } public bool OnlyShowMissingGatheredItems { get; set; }
public bool WrapAroundWhenReordering { get; set; } public bool ShowAssignmentChatMessages { get; set; } = true;
} }
} }

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

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

View File

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

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

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

@ -1 +1 @@
Subproject commit 7cb54772e3a4a60ad02520e898d1ed0e82b2a751 Subproject commit a63c8e7154e272374ffa03d5c801736d4229e38a

@ -1 +1 @@
Subproject commit f1c688a0599b41d70230021328a575da7351cf91 Subproject commit 677e28c0696eb13351d90d13ff27adb667b2c862

2
LLib

@ -1 +1 @@
Subproject commit 7f83ece6a0c07d0fbc1902502050563b5a8c09e4 Subproject commit fa3d19dde18bfd00237e66ea1eb48a60caa01b8a

View File

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