diff --git a/ARControl/ARControl.csproj b/ARControl/ARControl.csproj
index a5a4607..7134b01 100644
--- a/ARControl/ARControl.csproj
+++ b/ARControl/ARControl.csproj
@@ -48,6 +48,10 @@
$(DalamudLibPath)Newtonsoft.Json.dll
false
+
+ $(DalamudLibPath)FFXIVClientStructs.dll
+ false
+
$(AutoRetainerLibPath)AutoRetainerAPI.dll
diff --git a/ARControl/AutoRetainerControlPlugin.Sync.cs b/ARControl/AutoRetainerControlPlugin.Sync.cs
index 3f1c16b..51398fc 100644
--- a/ARControl/AutoRetainerControlPlugin.Sync.cs
+++ b/ARControl/AutoRetainerControlPlugin.Sync.cs
@@ -1,4 +1,5 @@
-using System.Linq;
+using System.Collections.Generic;
+using System.Linq;
namespace ARControl;
@@ -37,6 +38,7 @@ partial class AutoRetainerControlPlugin
save = true;
}
+ List seenRetainers = new();
foreach (var retainerData in offlineCharacterData.RetainerData)
{
var retainer = character.Retainers.SingleOrDefault(x => x.Name == retainerData.Name);
@@ -52,6 +54,8 @@ partial class AutoRetainerControlPlugin
character.Retainers.Add(retainer);
}
+ seenRetainers.Add(retainer.Name);
+
if (retainer.DisplayOrder != retainerData.DisplayOrder)
{
retainer.DisplayOrder = retainerData.DisplayOrder;
@@ -70,6 +74,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 +106,9 @@ partial class AutoRetainerControlPlugin
save = true;
}
}
+
+ if (character.Retainers.RemoveAll(x => !seenRetainers.Contains(x.Name)) > 0)
+ save = true;
}
if (save)
diff --git a/ARControl/AutoRetainerControlPlugin.cs b/ARControl/AutoRetainerControlPlugin.cs
index 3b141df..2f1ea32 100644
--- a/ARControl/AutoRetainerControlPlugin.cs
+++ b/ARControl/AutoRetainerControlPlugin.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
+using System.Runtime.CompilerServices;
using ARControl.GameData;
using ARControl.Windows;
using AutoRetainerAPI;
@@ -10,6 +12,7 @@ using Dalamud.Interface.Windowing;
using Dalamud.Plugin;
using Dalamud.Plugin.Services;
using ECommons;
+using FFXIVClientStructs.FFXIV.Client.Game;
using ImGuiNET;
namespace ARControl;
@@ -17,6 +20,7 @@ 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;
@@ -70,72 +74,199 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
}
private void SendRetainerToVenture(string retainerName)
+ {
+ var venture = GetNextVenture(retainerName, false);
+ if (venture == QuickVentureId)
+ _autoRetainerApi.SetVenture(0);
+ else if (venture.HasValue)
+ _autoRetainerApi.SetVenture(venture.Value);
+ }
+
+ private unsafe uint? GetNextVenture(string retainerName, bool dryRun)
{
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
if (ch == null)
{
_pluginLog.Information("No character information found");
+ return null;
}
- else if (ch.Type == Configuration.CharacterType.NotManaged)
+
+ 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();
+ var venturesInProgress = CalculateVenturesInProgress(ch);
+ foreach (var inpr in venturesInProgress)
+ {
+ _pluginLog.Information($"In Progress: {inpr.Key} → {inpr.Value}");
+ }
+
+ IReadOnlyList itemListIds;
+ if (ch.Type == Configuration.CharacterType.Standalone)
+ itemListIds = ch.ItemListIds;
else
{
- var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName);
- if (retainer == null)
+ var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId);
+ if (group == null)
{
- _pluginLog.Information("No retainer information found");
+ _pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}.");
+ return null;
}
- else if (!retainer.Managed)
+
+ 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()
+ .ToList();
+ InventoryManager* inventoryManager = InventoryManager.Instance();
+ foreach (var list in itemLists)
+ {
+ _pluginLog.Information($"Checking ventures in list '{list.Name}'");
+ IReadOnlyList itemsOnList;
+ if (list.Type == Configuration.ListType.CollectOneTime)
{
- _pluginLog.Information("Retainer is not managed");
+ itemsOnList = list.Items
+ .Select(x => new StockedItem
+ {
+ QueuedItem = x,
+ InventoryCount = 0,
+ })
+ .Where(x => x.RequestedCount > 0)
+ .ToList()
+ .AsReadOnly();
}
else
{
- _pluginLog.Information("Checking tasks...");
- Sync();
- /* FIXME
- foreach (var queuedItem in _configuration.QueuedItems.Where(x => x.RemainingQuantity > 0))
- {
- _pluginLog.Information($"Checking venture info for itemId {queuedItem.ItemId}");
-
- var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, queuedItem);
- if (reward == null)
+ itemsOnList = list.Items
+ .Select(x => new StockedItem
{
- _pluginLog.Information("Retainer can't complete venture");
- }
- 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);
+ QueuedItem = x,
+ InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) +
+ (venturesInProgress.TryGetValue(x.ItemId, out int inProgress) ? inProgress : 0),
+ })
+ .Where(x => x.InventoryCount <= x.RequestedCount)
+ .ToList()
+ .AsReadOnly();
- retainer.LastVenture = venture.RowId;
- queuedItem.RemainingQuantity =
- Math.Max(0, queuedItem.RemainingQuantity - reward.Quantity);
- _pluginInterface.SavePluginConfig(_configuration);
- return;
- }
- }
- */
+ // collect items with the least current inventory first
+ if (list.Priority == Configuration.ListPriority.Balanced)
+ itemsOnList = itemsOnList.OrderBy(x => x.InventoryCount).ToList().AsReadOnly();
+ }
- // fallback: managed but no venture found
- if (retainer.LastVenture != 395)
+ _pluginLog.Information($"Found {itemsOnList.Count} items on current list");
+ if (itemsOnList.Count == 0)
+ continue;
+
+ foreach (var itemOnList in itemsOnList)
+ {
+ _pluginLog.Information($"Checking venture info for itemId {itemOnList.ItemId}");
+
+ var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, itemOnList.ItemId);
+ if (venture == null || reward == null)
{
- _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);
+ _pluginLog.Information($"Retainer can't complete venture '{venture?.Name}'");
}
else
- _pluginLog.Information("Not changing venture plan, already 395");
+ {
+ _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}");
+
+ if (!dryRun)
+ {
+ retainer.HasVenture = true;
+ retainer.LastVenture = venture.RowId;
+
+ if (list.Type == Configuration.ListType.CollectOneTime)
+ {
+ itemOnList.RequestedCount =
+ Math.Max(0, itemOnList.RequestedCount - reward.Quantity);
+ }
+
+ _pluginInterface.SavePluginConfig(_configuration);
+ }
+
+ return venture.RowId;
+ }
+ }
+
+ }
+
+ // fallback: managed but no venture found
+ if (retainer.LastVenture != QuickVentureId)
+ {
+ _chatGui.Print($"[ARC] No tasks left for retainer {retainerName}, sending to Quick Venture.");
+ _pluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QC");
+
+ if (!dryRun)
+ {
+ retainer.HasVenture = true;
+ retainer.LastVenture = QuickVentureId;
+ _pluginInterface.SavePluginConfig(_configuration);
+ }
+
+ return QuickVentureId;
+ }
+ else
+ {
+ _pluginLog.Information("Not changing venture, already a quick venture");
+ return null;
+ }
+ }
+
+ ///
+ /// 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.
+ ///
+ private Dictionary CalculateVenturesInProgress(Configuration.CharacterConfiguration character)
+ {
+ Dictionary inProgress = new Dictionary();
+ 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)
@@ -174,6 +305,24 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
{
if (arguments == "sync")
Sync();
+ else if (arguments == "d")
+ {
+ 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 retainerName = ch.Retainers.OrderBy(x => x.DisplayOrder).First().Name;
+ 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();
}
@@ -191,4 +340,16 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin
_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;
+ }
+ }
}
diff --git a/ARControl/Configuration.cs b/ARControl/Configuration.cs
index 9105f7b..b97fe9e 100644
--- a/ARControl/Configuration.cs
+++ b/ARControl/Configuration.cs
@@ -97,6 +97,7 @@ internal sealed class Configuration : IPluginConfiguration
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; }
diff --git a/ARControl/GameData/VentureResolver.cs b/ARControl/GameData/VentureResolver.cs
index b7fa025..5b6802a 100644
--- a/ARControl/GameData/VentureResolver.cs
+++ b/ARControl/GameData/VentureResolver.cs
@@ -15,18 +15,18 @@ 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.Information($"No applicable venture found for itemId {itemId}");
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");
@@ -34,7 +34,7 @@ internal sealed class VentureResolver
}
_pluginLog.Information(
- $"Found venture {venture.Name}, row = {venture.RowId}, checking if it is suitable");
+ $"Found venture {venture.Name}, row = {venture.RowId}, checking if we have high enough stats");
VentureReward? reward = null;
if (venture.CategoryName is "MIN" or "BTN")
{