diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..9124e51 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "LLib"] + path = LLib + url = git@git.carvel.li:liza/LLib.git diff --git a/ARControl.sln b/ARControl.sln index 1450d62..ea815c0 100644 --- a/ARControl.sln +++ b/ARControl.sln @@ -2,6 +2,8 @@ Microsoft Visual Studio Solution File, Format Version 12.00 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ARControl", "ARControl\ARControl.csproj", "{B33BF820-56C2-45A1-AEEC-3DCF526DBF42}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LLib", "LLib\LLib.csproj", "{C00249D7-E550-4A3F-937B-D938D1D46B8A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -12,5 +14,9 @@ Global {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.Build.0 = Debug|Any CPU {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.ActiveCfg = Release|Any CPU {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Release|Any CPU.Build.0 = Release|Any CPU + {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C00249D7-E550-4A3F-937B-D938D1D46B8A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/ARControl/ARControl.csproj b/ARControl/ARControl.csproj index 7256420..1e8538a 100644 --- a/ARControl/ARControl.csproj +++ b/ARControl/ARControl.csproj @@ -1,7 +1,7 @@ net7.0-windows - 1.0 + 2.0 11.0 enable true @@ -16,13 +16,17 @@ $(appdata)\XIVLauncher\addon\Hooks\dev\ - $(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.2.0.6\ + $(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.2.1.0\ $(DALAMUD_HOME)/ + + + + @@ -48,6 +52,10 @@ $(DalamudLibPath)Newtonsoft.Json.dll false + + $(DalamudLibPath)FFXIVClientStructs.dll + false + $(AutoRetainerLibPath)AutoRetainerAPI.dll @@ -57,7 +65,6 @@ - diff --git a/ARControl/AutoRetainerControlPlugin.Sync.cs b/ARControl/AutoRetainerControlPlugin.Sync.cs index 45a5fa4..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; @@ -24,7 +25,7 @@ partial class AutoRetainerControlPlugin LocalContentId = registeredCharacterId, CharacterName = offlineCharacterData.Name, WorldName = offlineCharacterData.World, - Managed = false, + Type = Configuration.CharacterType.NotManaged, }; save = true; @@ -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 e425186..02e7a21 100644 --- a/ARControl/AutoRetainerControlPlugin.cs +++ b/ARControl/AutoRetainerControlPlugin.cs @@ -1,16 +1,20 @@ 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; -using Dalamud.Interface.Components; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; using ECommons; +using FFXIVClientStructs.FFXIV.Client.Game; using ImGuiNET; namespace ARControl; @@ -18,6 +22,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; @@ -28,12 +33,14 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin 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, IPluginLog pluginLog) + IClientState clientState, IChatGui chatGui, ICommandManager commandManager, ITextureProvider textureProvider, + IPluginLog pluginLog) { _pluginInterface = pluginInterface; _clientState = clientState; @@ -41,11 +48,15 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin _commandManager = commandManager; _pluginLog = pluginLog; - _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); + _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration { Version = 2 }; _gameCache = new GameCache(dataManager); + _iconCache = new IconCache(textureProvider); _ventureResolver = new VentureResolver(_gameCache, _pluginLog); - _configWindow = new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _pluginLog); + _configWindow = + new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager, _iconCache, + _pluginLog) + { IsOpen = true }; _windowSystem.AddWindow(_configWindow); ECommonsMain.Init(_pluginInterface, this); @@ -66,77 +77,237 @@ 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.Managed) + + if (ch.Type == Configuration.CharacterType.NotManaged) { _pluginLog.Information("Character is not managed"); + return null; } + + var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName); + if (retainer == null) + { + _pluginLog.Information("No retainer information found"); + return null; + } + + if (!retainer.Managed) + { + _pluginLog.Information("Retainer is not managed"); + return null; + } + + _pluginLog.Information("Checking tasks..."); + Sync(); + 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(); - 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( + 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 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) { Configuration.CharacterConfiguration? characterConfiguration = _configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId); - if (characterConfiguration is not { Managed: true }) + if (characterConfiguration is not { Type: not Configuration.CharacterType.NotManaged }) return; Configuration.RetainerConfiguration? retainer = @@ -145,7 +316,19 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin return; ImGui.SameLine(); - ImGuiComponents.IconButton(FontAwesomeIcon.Book); + ImGui.Text(SeIconChar.Collectible.ToIconString()); + if (ImGui.IsItemHovered()) + { + string text = "This retainer is managed by ARC."; + + if (characterConfiguration.Type == Configuration.CharacterType.PartOfCharacterGroup) + { + var group = _configuration.CharacterGroups.Single(x => x.Id == characterConfiguration.CharacterGroupId); + text += $"\n\nCharacter Group: {group.Name}"; + } + + ImGui.SetTooltip(text); + } } private void TerritoryChanged(ushort e) => Sync(); @@ -154,6 +337,25 @@ 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(); } @@ -167,7 +369,21 @@ public sealed partial class AutoRetainerControlPlugin : IDalamudPlugin _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; + } + } } diff --git a/ARControl/Configuration.cs b/ARControl/Configuration.cs index 06eae06..6601c7c 100644 --- a/ARControl/Configuration.cs +++ b/ARControl/Configuration.cs @@ -1,14 +1,49 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Dalamud.Configuration; +using Dalamud.Game.Text; namespace ARControl; internal sealed class Configuration : IPluginConfiguration { - public int Version { get; set; } = 1; + public int Version { get; set; } - public List QueuedItems { get; set; } = new(); public List Characters { get; set; } = new(); + public List ItemLists { get; set; } = new(); + public List CharacterGroups { get; set; } = new(); + + public sealed class ItemList + { + public required Guid Id { get; set; } + public required string Name { get; set; } + public required ListType Type { get; set; } = ListType.CollectOneTime; + public required ListPriority Priority { get; set; } = ListPriority.InOrder; + public List Items { get; set; } = new(); + + public string GetIcon() + { + return Type switch + { + ListType.CollectOneTime => SeIconChar.BoxedNumber1.ToIconString(), + ListType.KeepStocked when Priority == ListPriority.Balanced => SeIconChar.EurekaLevel.ToIconString(), + ListType.KeepStocked => SeIconChar.Circle.ToIconString(), + _ => string.Empty + }; + } + } + + public enum ListType + { + CollectOneTime, + KeepStocked, + } + + public enum ListPriority + { + InOrder, + Balanced, + } public sealed class QueuedItem { @@ -16,12 +51,22 @@ internal sealed class Configuration : IPluginConfiguration public required int RemainingQuantity { get; set; } } + public sealed class CharacterGroup + { + public required Guid Id { get; set; } + public required string Name { get; set; } + public List ItemListIds { get; set; } = new(); + } + public sealed class CharacterConfiguration { public required ulong LocalContentId { get; set; } public required string CharacterName { get; set; } public required string WorldName { get; set; } - public required bool Managed { get; set; } + + public CharacterType Type { get; set; } = CharacterType.NotManaged; + public Guid CharacterGroupId { get; set; } + public List ItemListIds { get; set; } = new(); public List Retainers { get; set; } = new(); public HashSet GatheredItems { get; set; } = new(); @@ -29,6 +74,21 @@ internal sealed class Configuration : IPluginConfiguration public override string ToString() => $"{CharacterName} @ {WorldName}"; } + public enum CharacterType + { + NotManaged, + + /// + /// The character's item list(s) are manually selected. + /// + Standalone, + + /// + /// All item lists are managed through the character group. + /// + PartOfCharacterGroup + } + public sealed class RetainerConfiguration { public required string Name { get; set; } @@ -36,6 +96,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/Venture.cs b/ARControl/GameData/Venture.cs index 433ef57..5037cbb 100644 --- a/ARControl/GameData/Venture.cs +++ b/ARControl/GameData/Venture.cs @@ -14,6 +14,7 @@ internal sealed class Venture var taskDetails = dataManager.GetExcelSheet()!.GetRow(retainerTask.Task)!; var taskParameters = retainerTask.RetainerTaskParameter.Value!; ItemId = taskDetails.Item.Row; + IconId = taskDetails.Item.Value!.Icon; Name = taskDetails.Item.Value!.Name.ToString(); Level = retainerTask.RetainerLevel; ItemLevelCombat = retainerTask.RequiredItemLevel; @@ -76,6 +77,7 @@ internal sealed class Venture } public uint ItemId { get; } + public ushort IconId { get; } public string Name { get; } public byte Level { get; } public ushort ItemLevelCombat { get; } 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") { diff --git a/ARControl/IconCache.cs b/ARControl/IconCache.cs new file mode 100644 index 0000000..ccc9c07 --- /dev/null +++ b/ARControl/IconCache.cs @@ -0,0 +1,53 @@ +using System; +using System.Collections.Generic; +using Dalamud.Interface.Internal; +using Dalamud.Plugin.Services; + +namespace ARControl; + +internal sealed class IconCache : IDisposable +{ + private readonly ITextureProvider _textureProvider; + private readonly Dictionary _textureWraps = new(); + + public IconCache(ITextureProvider textureProvider) + { + _textureProvider = textureProvider; + } + + public IDalamudTextureWrap? GetIcon(uint iconId) + { + if (_textureWraps.TryGetValue(iconId, out TextureContainer? container)) + return container.Texture; + + var iconTex = _textureProvider.GetIcon(iconId); + if (iconTex != null) + { + if (iconTex.ImGuiHandle != nint.Zero) + { + _textureWraps[iconId] = new TextureContainer { Texture = iconTex }; + return iconTex; + } + + iconTex.Dispose(); + } + + _textureWraps[iconId] = new TextureContainer { Texture = null }; + return null; + } + + public void Dispose() + { + foreach (TextureContainer container in _textureWraps.Values) + container.Dispose(); + + _textureWraps.Clear(); + } + + private sealed class TextureContainer : IDisposable + { + public required IDalamudTextureWrap? Texture { get; init; } + + public void Dispose() => Texture?.Dispose(); + } +} diff --git a/ARControl/Windows/ConfigWindow.cs b/ARControl/Windows/ConfigWindow.cs index c1cb8be..ea04b5c 100644 --- a/ARControl/Windows/ConfigWindow.cs +++ b/ARControl/Windows/ConfigWindow.cs @@ -1,36 +1,55 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; using System.Numerics; using ARControl.GameData; +using Dalamud.Game.Text; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Internal; using Dalamud.Interface.Windowing; using Dalamud.Plugin; using Dalamud.Plugin.Services; +using ECommons; using ECommons.ImGuiMethods; using ImGuiNET; +using LLib; namespace ARControl.Windows; internal sealed class ConfigWindow : Window { - private const byte MaxLevel = 90; + // TODO This should also allow retainers under max level + private const byte MinLevel = 10; private static readonly Vector4 ColorGreen = ImGuiColors.HealerGreen; private static readonly Vector4 ColorRed = ImGuiColors.DalamudRed; private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey; + private 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 readonly DalamudPluginInterface _pluginInterface; private readonly Configuration _configuration; private readonly GameCache _gameCache; private readonly IClientState _clientState; private readonly ICommandManager _commandManager; + private readonly IconCache _iconCache; private readonly IPluginLog _pluginLog; + private readonly Dictionary _currentEditPopups = new(); private string _searchString = string.Empty; - private Configuration.QueuedItem? _dragDropSource; - private bool _enableDragDrop; + private TemporaryConfig _newGroup = new() { Name = string.Empty }; + + private TemporaryConfig _newList = new() + { + Name = string.Empty, + ListType = Configuration.ListType.CollectOneTime, + ListPriority = Configuration.ListPriority.InOrder + }; + private bool _checkPerCharacter = true; private bool _onlyShowMissing = true; @@ -40,159 +59,344 @@ internal sealed class ConfigWindow : Window GameCache gameCache, IClientState clientState, ICommandManager commandManager, + IconCache iconCache, IPluginLog pluginLog) - : base("ARC###ARControlConfig") + : base($"ARC {SeIconChar.Collectible.ToIconString()}###ARControlConfig") { _pluginInterface = pluginInterface; _configuration = configuration; _gameCache = gameCache; _clientState = clientState; _commandManager = commandManager; + _iconCache = iconCache; _pluginLog = pluginLog; + + SizeConstraints = new() + { + MinimumSize = new Vector2(480, 300), + MaximumSize = new Vector2(9999, 9999), + }; } public override void Draw() { + LImGui.AddPatreonIcon(_pluginInterface); + if (ImGui.BeginTabBar("ARConfigTabs")) { - DrawItemQueue(); + DrawVentureLists(); + DrawCharacterGroups(); DrawCharacters(); DrawGatheredItemsToCheck(); ImGui.EndTabBar(); } } - private unsafe void DrawItemQueue() + private void DrawVentureLists() { - if (ImGui.BeginTabItem("Venture Queue")) + if (ImGui.BeginTabItem("Venture Lists")) { - if (ImGui.BeginCombo("Add Item...##VentureSelection", "")) + Configuration.ItemList? listToDelete = null; + foreach (var list in _configuration.ItemLists) { - ImGuiEx.SetNextItemFullWidth(); - ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256); + ImGui.PushID($"List{list.Id}"); - foreach (var ventures in _gameCache.Ventures - .Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) - .OrderBy(x => x.Level) - .ThenBy(x => x.Name) - .ThenBy(x => x.ItemId) - .GroupBy(x => x.ItemId)) + if (ImGuiComponents.IconButton(FontAwesomeIcon.Cog)) { - var venture = ventures.First(); - if (ImGui.Selectable( - $"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})##SelectVenture{venture.RowId}")) + _currentEditPopups[list.Id] = new TemporaryConfig { - _configuration.QueuedItems.Add(new Configuration.QueuedItem - { - ItemId = venture.ItemId, - RemainingQuantity = 0, - }); - _searchString = string.Empty; - Save(); - } + Name = list.Name, + ListType = list.Type, + ListPriority = list.Priority, + }; + ImGui.OpenPopup($"##EditList{list.Id}"); } - ImGui.EndCombo(); - } + DrawVentureListEditorPopup(list, ref listToDelete); - ImGui.Separator(); + ImGui.SameLine(); - ImGui.Indent(30); + string label = $"{list.Name} {list.GetIcon()}"; - Configuration.QueuedItem? itemToRemove = null; - Configuration.QueuedItem? itemToAdd = null; - int indexToAdd = 0; - for (int i = 0; i < _configuration.QueuedItems.Count; ++i) - { - var item = _configuration.QueuedItems[i]; - ImGui.PushID($"QueueItem{i}"); - var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList(); - var venture = ventures.First(); - - if (!_enableDragDrop) + if (ImGui.CollapsingHeader(label)) { - ImGui.SetNextItemWidth(130); - int quantity = item.RemainingQuantity; - if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})", - ref quantity, 100)) - { - item.RemainingQuantity = quantity; - Save(); - } - } - else - { - ImGui.Selectable($"{item.RemainingQuantity}x {venture.Name}"); - - if (ImGui.BeginDragDropSource()) - { - ImGui.SetDragDropPayload("ArcDragDrop", nint.Zero, 0); - _dragDropSource = item; - - ImGui.EndDragDropSource(); - } - - if (ImGui.BeginDragDropTarget()) - { - if (_dragDropSource != null && ImGui.AcceptDragDropPayload("ArcDragDrop").NativePtr != null) - { - itemToAdd = _dragDropSource; - indexToAdd = i; - - _dragDropSource = null; - } - - ImGui.EndDragDropTarget(); - } - } - - ImGui.OpenPopupOnItemClick($"###ctx{i}", ImGuiPopupFlags.MouseButtonRight); - if (ImGui.BeginPopupContextItem($"###ctx{i}")) - { - if (ImGui.MenuItem($"Remove {venture.Name}")) - itemToRemove = item; - - ImGui.EndPopup(); + ImGui.Indent(30); + DrawVentureListItemSelection(list); + ImGui.Unindent(30); } ImGui.PopID(); } - if (itemToRemove != null) + if (listToDelete != null) { - _configuration.QueuedItems.Remove(itemToRemove); + _configuration.ItemLists.Remove(listToDelete); Save(); } - if (itemToAdd != null) - { - _pluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}"); - _configuration.QueuedItems.Remove(itemToAdd); - _configuration.QueuedItems.Insert(indexToAdd, itemToAdd); - Save(); - } - - ImGui.Unindent(30); - - if (_configuration.QueuedItems.Count > 0) - ImGui.Separator(); - - if (ImGuiComponents.IconButtonWithText(_enableDragDrop ? FontAwesomeIcon.Times : FontAwesomeIcon.Sort, _enableDragDrop ? "Disable Drag&Drop" : "Enable Drag&Drop")) - { - _enableDragDrop = !_enableDragDrop; - } - - ImGui.SameLine(); - if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Check, "Remove all finished items")) - { - if (_configuration.QueuedItems.RemoveAll(q => q.RemainingQuantity == 0) > 0) - Save(); - } - + ImGui.Separator(); + DrawNewVentureList(); ImGui.EndTabItem(); } } + 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)); + 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; + else + list.Priority = temporaryConfig.ListPriority; + + ImGui.CloseCurrentPopup(); + Save(); + } + 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 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, + }); + + _newList = new() + { + Name = string.Empty, + ListType = Configuration.ListType.CollectOneTime, + ListPriority = Configuration.ListPriority.InOrder + }; + + ImGui.CloseCurrentPopup(); + Save(); + } + + ImGui.EndPopup(); + } + } + + private (bool Save, bool CanSave) DrawVentureListEditor(TemporaryConfig temporaryConfig, + Configuration.ItemList? list) + { + 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}"); + 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}"); + int priority = (int)temporaryConfig.ListPriority; + if (ImGui.Combo("", ref priority, PriorityLabels, PriorityLabels.Length)) + temporaryConfig.ListPriority = (Configuration.ListPriority)priority; + ImGui.PopID(); + } + + return (save, canSave); + } + + private void DrawVentureListItemSelection(Configuration.ItemList list) + { + ImGuiEx.SetNextItemFullWidth(); + if (ImGui.BeginCombo($"##VentureSelection{list.Id}", "Add Venture...", ImGuiComboFlags.HeightLarge)) + { + ImGuiEx.SetNextItemFullWidth(); + ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256, ImGuiInputTextFlags.AutoSelectAll); + + foreach (var ventures in _gameCache.Ventures + .Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) + .OrderBy(x => x.Level) + .ThenBy(x => x.Name) + .ThenBy(x => x.ItemId) + .GroupBy(x => x.ItemId)) + { + var venture = ventures.First(); + IDalamudTextureWrap? icon = _iconCache.GetIcon(venture.IconId); + if (icon != null) + { + ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); + ImGui.SameLine(); + ImGui.SetCursorPosY(ImGui.GetCursorPosY() + 3); + } + + if (ImGui.Selectable( + $"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})##SelectVenture{venture.RowId}")) + { + list.Items.Add(new Configuration.QueuedItem + { + ItemId = venture.ItemId, + RemainingQuantity = 0, + }); + Save(); + } + } + + ImGui.EndCombo(); + } + + ImGui.Spacing(); + + Configuration.QueuedItem? itemToRemove = null; + Configuration.QueuedItem? itemToAdd = null; + int indexToAdd = 0; + float windowX = ImGui.GetContentRegionAvail().X; + for (int i = 0; i < list.Items.Count; ++i) + { + var item = list.Items[i]; + ImGui.PushID($"QueueItem{i}"); + var ventures = _gameCache.Ventures.Where(x => x.ItemId == item.ItemId).ToList(); + var venture = ventures.First(); + + IDalamudTextureWrap? icon = _iconCache.GetIcon(venture.IconId); + if (icon != null) + { + ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); + ImGui.SameLine(0, 3); + } + + ImGui.SetNextItemWidth(130); + int quantity = item.RemainingQuantity; + if (ImGui.InputInt($"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})", + ref quantity, 100)) + { + item.RemainingQuantity = quantity; + Save(); + } + + ImGui.SameLine(windowX - 30); + ImGui.BeginDisabled(i == 0); + if (ImGuiComponents.IconButton($"##Up{i}", FontAwesomeIcon.ArrowUp)) + { + itemToAdd = item; + indexToAdd = i - 1; + } + + ImGui.EndDisabled(); + + ImGui.SameLine(0, 0); + ImGui.BeginDisabled(i == list.Items.Count - 1); + if (ImGuiComponents.IconButton($"##Down{i}", FontAwesomeIcon.ArrowDown)) + { + itemToAdd = item; + indexToAdd = i + 1; + } + + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) + itemToRemove = item; + + ImGui.PopID(); + } + + if (itemToRemove != null) + { + list.Items.Remove(itemToRemove); + Save(); + } + + if (itemToAdd != null) + { + _pluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}"); + list.Items.Remove(itemToAdd); + list.Items.Insert(indexToAdd, itemToAdd); + Save(); + } + + if (list.Items.Count > 0 && list.Type == Configuration.ListType.CollectOneTime) + { + ImGui.Spacing(); + 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); + Save(); + } + + ImGui.EndDisabled(); + } + + ImGui.Spacing(); + } + private void DrawCharacters() { if (ImGui.BeginTabItem("Retainers")) @@ -208,9 +412,9 @@ internal sealed class ConfigWindow : Window { ImGui.PushID($"Char{character.LocalContentId}"); - ImGui.PushItemWidth(ImGui.GetFontSize() * 30); + ImGui.SetNextItemWidth(ImGui.GetFontSize() * 30); Vector4 buttonColor = new Vector4(); - if (character is { Managed: true, Retainers.Count: > 0 }) + if (character is { Type: not Configuration.CharacterType.NotManaged, Retainers.Count: > 0 }) { if (character.Retainers.All(x => x.Managed)) buttonColor = ImGuiColors.HealerGreen; @@ -222,33 +426,129 @@ internal sealed class ConfigWindow : Window if (ImGuiComponents.IconButton(FontAwesomeIcon.Book, buttonColor)) { - character.Managed = !character.Managed; + 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; + } + Save(); } ImGui.SameLine(); if (ImGui.CollapsingHeader( - $"{character.CharacterName} {(character.Managed ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}")) + $"{character.CharacterName} {(character.Type != Configuration.CharacterType.NotManaged ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}")) { ImGui.Indent(30); - foreach (var retainer in character.Retainers.Where(x => x.Job > 0).OrderBy(x => x.DisplayOrder)) - { - ImGui.BeginDisabled(retainer.Level < MaxLevel); - bool managed = retainer.Managed && retainer.Level == MaxLevel; - ImGui.Text(_gameCache.Jobs[retainer.Job]); - ImGui.SameLine(); - if (ImGui.Checkbox($"{retainer.Name}###Retainer{retainer.Name}{retainer.DisplayOrder}", - ref managed)) + 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(); + + + if (ImGui.BeginTabBar("CharOptions")) + { + if (character.Type != Configuration.CharacterType.NotManaged && + ImGui.BeginTabItem("Venture Lists")) { - retainer.Managed = managed; - Save(); + 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; + } + + Save(); + } + + ImGui.Separator(); + if (groupIndex == 0) + { + // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract + if (character.ItemListIds == null) + character.ItemListIds = new(); + DrawVentureListSelection(character.LocalContentId.ToString(), + character.ItemListIds); + } + else + { + ImGui.TextWrapped($"Retainers will participate in the following lists:"); + ImGui.Indent(30); + + 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() + .ToList(); + if (lists.Count > 0) + { + foreach (var list in lists) + ImGui.BulletText($"{list.Name}"); + } + else + ImGui.TextColored(ImGuiColors.DalamudRed, "(None)"); + + ImGui.Unindent(30); + ImGui.Spacing(); + } + + ImGui.EndTabItem(); } - ImGui.EndDisabled(); + if (ImGui.BeginTabItem("Retainers")) + { + foreach (var retainer in character.Retainers.Where(x => x.Job > 0) + .OrderBy(x => x.DisplayOrder)) + { + ImGui.BeginDisabled(retainer.Level < MinLevel); + + bool managed = retainer.Managed && retainer.Level >= MinLevel; + + IDalamudTextureWrap? icon = _iconCache.GetIcon(62000 + retainer.Job); + if (icon != null) + { + ImGui.Image(icon.ImGuiHandle, new Vector2(23, 23)); + ImGui.SameLine(); + } + + if (ImGui.Checkbox( + $"{retainer.Name}###Retainer{retainer.Name}{retainer.DisplayOrder}", + ref managed)) + { + retainer.Managed = managed; + Save(); + } + + ImGui.EndDisabled(); + } + + ImGui.EndTabItem(); + } + + ImGui.EndTabBar(); } + ImGui.Unindent(30); } @@ -260,6 +560,186 @@ internal sealed class ConfigWindow : Window } } + private void DrawCharacterGroups() + { + if (ImGui.BeginTabItem("Groups")) + { + 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); + Save(); + } + + ImGui.Separator(); + DrawNewCharacterGroup(); + ImGui.EndTabItem(); + } + } + + private void DrawCharacterGroupEditorPopup(Configuration.CharacterGroup group, + out List 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(); + Save(); + } + 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 assignedCharacters) + { + string countLabel = assignedCharacters.Count == 0 ? "no characters" + : assignedCharacters.Count == 1 ? "1 character" + : $"{assignedCharacters.Count} characters"; + if (ImGui.CollapsingHeader($"{group.Name} ({countLabel})")) + { + ImGui.Indent(30); + if (ImGui.BeginTabBar("GroupOptions")) + { + if (ImGui.BeginTabItem("Venture Lists")) + { + DrawVentureListSelection(group.Id.ToString(), group.ItemListIds); + ImGui.EndTabItem(); + } + + if (ImGui.BeginTabItem("Characters")) + { + ImGui.Text("Characters in this group:"); + ImGui.Indent(30); + foreach (var character in assignedCharacters.OrderBy(x => x.WorldName) + .ThenBy(x => x.LocalContentId)) + ImGui.TextUnformatted($"{character.CharacterName} @ {character.WorldName}"); + ImGui.Unindent(30); + } + + ImGui.EndTabBar(); + } + + ImGui.Unindent(30); + } + } + + 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(); + Save(); + } + + 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('%') && + !_configuration.CharacterGroups.Any(x => x != existingGroup && name.EqualsIgnoreCase(x.Name)); + } + + private bool IsValidListName(string name, Configuration.ItemList? existingList) + { + return name.Length >= 2 && + !name.Contains('%') && + !_configuration.ItemLists.Any(x => x != existingList && name.EqualsIgnoreCase(x.Name)); + } + private void DrawGatheredItemsToCheck() { if (ImGui.BeginTabItem("Locked Items")) @@ -269,7 +749,8 @@ internal sealed class ConfigWindow : Window ImGui.Separator(); var itemsToCheck = - _configuration.QueuedItems + _configuration.ItemLists + .SelectMany(x => x.Items) .Select(x => x.ItemId) .Distinct() .Select(itemId => new @@ -287,10 +768,10 @@ internal sealed class ConfigWindow : Window .ToList(); var charactersToCheck = _configuration.Characters - .Where(x => x.Managed) + .Where(x => x.Type != Configuration.CharacterType.NotManaged) .OrderBy(x => x.WorldName) .ThenBy(x => x.LocalContentId) - .Select(x => new CheckedCharacter(x, itemsToCheck)) + .Select(x => new CheckedCharacter(_configuration, x, itemsToCheck)) .ToList(); if (_checkPerCharacter) @@ -379,6 +860,129 @@ internal sealed class ConfigWindow : Window } } + private void DrawVentureListSelection(string id, List selectedLists) + { + ImGui.PushID($"##ListSelection{id}"); + + List<(Guid Id, string Name, Configuration.ItemList List)> itemLists = new List + { + new Configuration.ItemList + { + Id = Guid.Empty, + Name = "---", + Type = Configuration.ListType.CollectOneTime, + Priority = Configuration.ListPriority.InOrder, + } + }.Concat(_configuration.ItemLists) + .Select(x => (x.Id, x.Name, x)).ToList(); + int? itemToRemove = null; + int? itemToAdd = null; + int indexToAdd = 0; + float windowX = ImGui.GetContentRegionAvail().X; + for (int i = 0; i < selectedLists.Count; ++i) + { + ImGui.PushID($"##{id}_Item{i}"); + var listId = selectedLists[i]; + var listIndex = itemLists.FindIndex(x => x.Id == listId); + + ImGui.SetNextItemWidth(windowX - 76); + if (ImGui.Combo("", ref listIndex, itemLists.Select(x => x.Name).ToArray(), itemLists.Count)) + { + selectedLists[i] = itemLists[listIndex].Id; + Save(); + } + + ImGui.SameLine(); + ImGui.BeginDisabled(i == 0); + if (ImGuiComponents.IconButton($"##Up{i}", FontAwesomeIcon.ArrowUp)) + { + itemToAdd = i; + indexToAdd = i - 1; + } + + ImGui.EndDisabled(); + + ImGui.SameLine(0, 0); + ImGui.BeginDisabled(i == selectedLists.Count - 1); + if (ImGuiComponents.IconButton($"##Down{i}", FontAwesomeIcon.ArrowDown)) + { + itemToAdd = i; + indexToAdd = i + 1; + } + + ImGui.EndDisabled(); + + ImGui.SameLine(); + if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) + itemToRemove = i; + + if (listIndex > 0) + { + if (selectedLists.Take(i).Any(x => x == listId)) + { + ImGui.Indent(30); + ImGui.TextColored(ImGuiColors.DalamudYellow, "This entry is a duplicate and will be ignored."); + ImGui.Unindent(30); + } + else + { + var list = itemLists[listIndex].List; + ImGui.Indent(30); + ImGui.Text(list.Type == Configuration.ListType.CollectOneTime + ? $"{list.GetIcon()} Items on this list will be collected once." + : $"{list.GetIcon()} Items on this list will be kept in stock on each character."); + ImGui.Spacing(); + foreach (var item in list.Items) + { + var venture = _gameCache.Ventures.First(x => x.ItemId == item.ItemId); + ImGui.Text($"{item.RemainingQuantity}x {venture.Name}"); + } + + ImGui.Unindent(30); + } + } + + ImGui.PopID(); + } + + if (itemToRemove != null) + { + selectedLists.RemoveAt(itemToRemove.Value); + Save(); + } + + if (itemToAdd != null) + { + Guid listId = selectedLists[itemToAdd.Value]; + selectedLists.RemoveAt(itemToAdd.Value); + selectedLists.Insert(indexToAdd, listId); + Save(); + } + + var unusedLists = itemLists.Where(x => x.Id != Guid.Empty && !selectedLists.Contains(x.Id)).ToList(); + ImGui.BeginDisabled(unusedLists.Count == 0); + if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Plus, "Add Venture List to this Group")) + ImGui.OpenPopup($"##AddItem{id}"); + + if (ImGui.BeginPopupContextItem($"##AddItem{id}")) + { + foreach (var list in unusedLists) + { + if (ImGui.MenuItem($"{list.Name}##{list.Id}")) + { + selectedLists.Add(list.Id); + Save(); + } + } + + ImGui.EndPopup(); + } + + ImGui.EndDisabled(); + + ImGui.PopID(); + } + private void Save() { _pluginInterface.SavePluginConfig(_configuration); @@ -386,16 +990,44 @@ internal sealed class ConfigWindow : Window private sealed class CheckedCharacter { - public CheckedCharacter(Configuration.CharacterConfiguration character, + public CheckedCharacter(Configuration configuration, Configuration.CharacterConfiguration character, List itemsToCheck) { Character = character; + List 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 @@ -423,4 +1055,11 @@ internal sealed class ConfigWindow : Window public required List Ventures { get; init; } public required uint ItemId { get; init; } } + + private sealed class TemporaryConfig + { + public required string Name { get; set; } + public Configuration.ListType ListType { get; set; } + public Configuration.ListPriority ListPriority { get; set; } + } } diff --git a/ARControl/packages.lock.json b/ARControl/packages.lock.json index 6cf1c73..784f99f 100644 --- a/ARControl/packages.lock.json +++ b/ARControl/packages.lock.json @@ -7,6 +7,9 @@ "requested": "[2.1.12, )", "resolved": "2.1.12", "contentHash": "Sc0PVxvgg4NQjcI8n10/VfUQBAS4O+Fw2pZrAqBdRMbthYGeogzu5+xmIGCGmsEZ/ukMOBuAqiNiB5qA3MRalg==" + }, + "llib": { + "type": "Project" } } } diff --git a/LLib b/LLib new file mode 160000 index 0000000..abbbec4 --- /dev/null +++ b/LLib @@ -0,0 +1 @@ +Subproject commit abbbec4f26b1a8903b0cd7aa04f00d557602eaf3