431 lines
17 KiB
C#
431 lines
17 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using ARControl.GameData;
|
|
using ARControl.Windows;
|
|
using AutoRetainerAPI;
|
|
using Dalamud.Game.Command;
|
|
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 IClientState _clientState;
|
|
private readonly IChatGui _chatGui;
|
|
private readonly ICommandManager _commandManager;
|
|
private readonly IPluginLog _pluginLog;
|
|
|
|
private readonly Configuration _configuration;
|
|
private readonly GameCache _gameCache;
|
|
private readonly IconCache _iconCache;
|
|
private readonly VentureResolver _ventureResolver;
|
|
private readonly ConfigWindow _configWindow;
|
|
private readonly AutoRetainerApi _autoRetainerApi;
|
|
|
|
public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, IDataManager dataManager,
|
|
IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider,
|
|
IPluginLog pluginLog)
|
|
{
|
|
_pluginInterface = pluginInterface;
|
|
_clientState = clientState;
|
|
_chatGui = chatGui;
|
|
_commandManager = commandManager;
|
|
_pluginLog = pluginLog;
|
|
|
|
_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, _iconCache,
|
|
_pluginLog);
|
|
_windowSystem.AddWindow(_configWindow);
|
|
|
|
ECommonsMain.Init(_pluginInterface, this);
|
|
_autoRetainerApi = new();
|
|
|
|
_pluginInterface.UiBuilder.Draw += _windowSystem.Draw;
|
|
_pluginInterface.UiBuilder.OpenConfigUi += _configWindow.Toggle;
|
|
_autoRetainerApi.OnSendRetainerToVenture += SendRetainerToVenture;
|
|
_autoRetainerApi.OnRetainerPostVentureTaskDraw += RetainerTaskButtonDraw;
|
|
_clientState.TerritoryChanged += TerritoryChanged;
|
|
_commandManager.AddHandler("/arc", new CommandInfo(ProcessCommand)
|
|
{
|
|
HelpMessage = "Manage retainers"
|
|
});
|
|
|
|
if (_autoRetainerApi.Ready)
|
|
Sync();
|
|
}
|
|
|
|
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)
|
|
{
|
|
var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId);
|
|
if (ch == null)
|
|
{
|
|
_pluginLog.Information("No character information found");
|
|
return null;
|
|
}
|
|
|
|
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 venturesInProgress = CalculateVenturesInProgress(ch);
|
|
foreach (var inProgress in venturesInProgress)
|
|
{
|
|
_pluginLog.Verbose(
|
|
$"Venture In Progress: ItemId {inProgress.Key} for a total amount of {inProgress.Value}");
|
|
}
|
|
|
|
IReadOnlyList<Guid> itemListIds;
|
|
if (ch.Type == Configuration.CharacterType.Standalone)
|
|
itemListIds = ch.ItemListIds;
|
|
else
|
|
{
|
|
var group = _configuration.CharacterGroups.SingleOrDefault(x => x.Id == ch.CharacterGroupId);
|
|
if (group == null)
|
|
{
|
|
_pluginLog.Error($"Unable to resolve character group {ch.CharacterGroupId}.");
|
|
return null;
|
|
}
|
|
|
|
itemListIds = group.ItemListIds;
|
|
}
|
|
|
|
var itemLists = itemListIds.Where(listId => listId != Guid.Empty)
|
|
.Select(listId => _configuration.ItemLists.SingleOrDefault(x => x.Id == listId))
|
|
.Where(list => list != null)
|
|
.Cast<Configuration.ItemList>()
|
|
.ToList();
|
|
InventoryManager* inventoryManager = InventoryManager.Instance();
|
|
foreach (var list in itemLists)
|
|
{
|
|
_pluginLog.Information($"Checking ventures in list '{list.Name}'");
|
|
IReadOnlyList<StockedItem> itemsOnList;
|
|
if (list.Type == Configuration.ListType.CollectOneTime)
|
|
{
|
|
itemsOnList = list.Items
|
|
.Select(x => new StockedItem
|
|
{
|
|
QueuedItem = x,
|
|
InventoryCount = 0,
|
|
})
|
|
.Where(x => x.RequestedCount > 0)
|
|
.ToList()
|
|
.AsReadOnly();
|
|
}
|
|
else
|
|
{
|
|
itemsOnList = list.Items
|
|
.Select(x => new StockedItem
|
|
{
|
|
QueuedItem = x,
|
|
InventoryCount = inventoryManager->GetInventoryItemCount(x.ItemId) +
|
|
(venturesInProgress.TryGetValue(x.ItemId, out int inProgress)
|
|
? inProgress
|
|
: 0),
|
|
})
|
|
.Where(x => x.InventoryCount < x.RequestedCount)
|
|
.ToList()
|
|
.AsReadOnly();
|
|
|
|
// collect items with the least current inventory first
|
|
if (list.Priority == Configuration.ListPriority.Balanced)
|
|
itemsOnList = itemsOnList.OrderBy(x => x.InventoryCount).ToList().AsReadOnly();
|
|
}
|
|
|
|
_pluginLog.Debug($"Found {itemsOnList.Count} to-do items on current list");
|
|
if (itemsOnList.Count == 0)
|
|
continue;
|
|
|
|
foreach (var itemOnList in itemsOnList)
|
|
{
|
|
_pluginLog.Debug($"Checking venture info for itemId {itemOnList.ItemId}");
|
|
|
|
var (venture, reward) = _ventureResolver.ResolveVenture(ch, retainer, itemOnList.ItemId);
|
|
if (venture == null)
|
|
{
|
|
venture = _gameCache.Ventures.FirstOrDefault(x => x.ItemId == itemOnList.ItemId);
|
|
_pluginLog.Debug(
|
|
$"Retainer doesn't know how to gather itemId {itemOnList.ItemId} ({venture?.Name})");
|
|
}
|
|
else if (reward == null)
|
|
{
|
|
_pluginLog.Debug($"Retainer can't complete venture '{venture.Name}'");
|
|
}
|
|
else
|
|
{
|
|
_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}");
|
|
|
|
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(
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <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 { Type: not Configuration.CharacterType.NotManaged })
|
|
return;
|
|
|
|
Configuration.RetainerConfiguration? retainer =
|
|
characterConfiguration.Retainers.FirstOrDefault(x => x.Name == retainerName);
|
|
if (retainer is not { Managed: true })
|
|
return;
|
|
|
|
ImGui.SameLine();
|
|
ImGui.Text(SeIconChar.Collectible.ToIconString());
|
|
if (ImGui.IsItemHovered())
|
|
{
|
|
string text = "This retainer is managed by ARC.";
|
|
|
|
if (characterConfiguration.Type == Configuration.CharacterType.PartOfCharacterGroup)
|
|
{
|
|
var group = _configuration.CharacterGroups.Single(x => x.Id == characterConfiguration.CharacterGroupId);
|
|
text += $"\n\nCharacter Group: {group.Name}";
|
|
}
|
|
|
|
ImGui.SetTooltip(text);
|
|
}
|
|
}
|
|
|
|
private void TerritoryChanged(ushort e) => Sync();
|
|
|
|
private void ProcessCommand(string command, string arguments)
|
|
{
|
|
if (arguments == "sync")
|
|
Sync();
|
|
else if (arguments.StartsWith("dnv"))
|
|
{
|
|
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();
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
_commandManager.RemoveHandler("/arc");
|
|
_clientState.TerritoryChanged -= TerritoryChanged;
|
|
_autoRetainerApi.OnRetainerPostVentureTaskDraw -= RetainerTaskButtonDraw;
|
|
_autoRetainerApi.OnSendRetainerToVenture -= SendRetainerToVenture;
|
|
_pluginInterface.UiBuilder.OpenConfigUi -= _configWindow.Toggle;
|
|
_pluginInterface.UiBuilder.Draw -= _windowSystem.Draw;
|
|
|
|
_iconCache.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;
|
|
}
|
|
}
|
|
}
|