From 161bf7e39c427c8ffaffecde96dda5cba791f51f Mon Sep 17 00:00:00 2001 From: Liza Carvelli Date: Sat, 30 Sep 2023 13:19:46 +0200 Subject: [PATCH] Initial Commit --- .gitignore | 2 + ARControl.sln | 16 + ARControl/.gitignore | 3 + ARControl/ARControl.csproj | 83 +++++ ARControl/ARControl.json | 7 + ARControl/AutoRetainerControlPlugin.cs | 303 ++++++++++++++++++ ARControl/Configuration.cs | 45 +++ ARControl/DalamudPackager.targets | 21 ++ ARControl/GameData/GameCache.cs | 30 ++ ARControl/GameData/ItemToGather.cs | 19 ++ ARControl/GameData/Venture.cs | 93 ++++++ ARControl/GameData/VentureReward.cs | 9 + ARControl/Windows/ConfigWindow.cs | 408 +++++++++++++++++++++++++ ARControl/packages.lock.json | 19 ++ README.md | 13 + global.json | 7 + 16 files changed, 1078 insertions(+) create mode 100644 .gitignore create mode 100644 ARControl.sln create mode 100644 ARControl/.gitignore create mode 100644 ARControl/ARControl.csproj create mode 100644 ARControl/ARControl.json create mode 100644 ARControl/AutoRetainerControlPlugin.cs create mode 100644 ARControl/Configuration.cs create mode 100644 ARControl/DalamudPackager.targets create mode 100644 ARControl/GameData/GameCache.cs create mode 100644 ARControl/GameData/ItemToGather.cs create mode 100644 ARControl/GameData/Venture.cs create mode 100644 ARControl/GameData/VentureReward.cs create mode 100644 ARControl/Windows/ConfigWindow.cs create mode 100644 ARControl/packages.lock.json create mode 100644 README.md create mode 100644 global.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05dc549 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/.idea +*.user diff --git a/ARControl.sln b/ARControl.sln new file mode 100644 index 0000000..1450d62 --- /dev/null +++ b/ARControl.sln @@ -0,0 +1,16 @@ + +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 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B33BF820-56C2-45A1-AEEC-3DCF526DBF42}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection +EndGlobal diff --git a/ARControl/.gitignore b/ARControl/.gitignore new file mode 100644 index 0000000..958518b --- /dev/null +++ b/ARControl/.gitignore @@ -0,0 +1,3 @@ +/dist +/obj +/bin diff --git a/ARControl/ARControl.csproj b/ARControl/ARControl.csproj new file mode 100644 index 0000000..20978a1 --- /dev/null +++ b/ARControl/ARControl.csproj @@ -0,0 +1,83 @@ + + + net7.0-windows + 1.0 + 11.0 + enable + true + false + false + dist + true + portable + $(SolutionDir)=X:\ + true + + + + $(appdata)\XIVLauncher\addon\Hooks\dev\ + $(appdata)\XIVLauncher\installedPlugins\AutoRetainer\4.1.2.5\ + + + + $(DALAMUD_HOME)/ + + + + + + + + + + $(DalamudLibPath)Dalamud.dll + false + + + $(DalamudLibPath)ImGui.NET.dll + false + + + $(DalamudLibPath)ImGuiScene.dll + false + + + $(DalamudLibPath)Lumina.dll + false + + + $(DalamudLibPath)Lumina.Excel.dll + false + + + $(DalamudLibPath)Newtonsoft.Json.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(DalamudLibPath)FFXIVClientStructs.dll + false + + + $(AutoRetainerLibPath)AutoRetainerAPI.dll + + + $(AutoRetainerLibPath)ECommons.dll + + + $(AutoRetainerLibPath)ClickLib.dll + + + + + + + + + + + + diff --git a/ARControl/ARControl.json b/ARControl/ARControl.json new file mode 100644 index 0000000..27b4902 --- /dev/null +++ b/ARControl/ARControl.json @@ -0,0 +1,7 @@ +{ + "Name": "ARC", + "Author": "Liza Carvelli", + "Punchline": "Better AutoRetainer Venture Distribution", + "Description": "", + "RepoUrl": "https://git.carvel.li/liza/ARControl" +} diff --git a/ARControl/AutoRetainerControlPlugin.cs b/ARControl/AutoRetainerControlPlugin.cs new file mode 100644 index 0000000..c570e16 --- /dev/null +++ b/ARControl/AutoRetainerControlPlugin.cs @@ -0,0 +1,303 @@ +using System; +using System.Linq; +using ARControl.GameData; +using ARControl.Windows; +using AutoRetainerAPI; +using Dalamud.Data; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; +using Dalamud.Game.Gui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Plugin; +using ECommons; +using ImGuiNET; + +namespace ARControl; + +public sealed class AutoRetainerControlPlugin : IDalamudPlugin +{ + private readonly WindowSystem _windowSystem = new(nameof(AutoRetainerControlPlugin)); + + private readonly DalamudPluginInterface _pluginInterface; + private readonly DataManager _dataManager; + private readonly ClientState _clientState; + private readonly ChatGui _chatGui; + private readonly CommandManager _commandManager; + + private readonly Configuration _configuration; + private readonly GameCache _gameCache; + private readonly ConfigWindow _configWindow; + private readonly AutoRetainerApi _autoRetainerApi; + + public AutoRetainerControlPlugin(DalamudPluginInterface pluginInterface, DataManager dataManager, + ClientState clientState, ChatGui chatGui, CommandManager commandManager) + { + _pluginInterface = pluginInterface; + _dataManager = dataManager; + _clientState = clientState; + _chatGui = chatGui; + _commandManager = commandManager; + + _configuration = (Configuration?)_pluginInterface.GetPluginConfig() ?? new Configuration(); + + _gameCache = new GameCache(_dataManager); + _configWindow = new ConfigWindow(_pluginInterface, _configuration, _gameCache, _clientState, _commandManager); + _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)); + + if (_autoRetainerApi.Ready) + Sync(); + } + + public string Name => "ARC"; + + private void SendRetainerToVenture(string retainerName) + { + var ch = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == _clientState.LocalContentId); + if (ch == null) + { + PluginLog.Information("No character information found"); + } + else if (!ch.Managed) + { + PluginLog.Information("Character is not managed"); + } + else + { + var retainer = ch.Retainers.SingleOrDefault(x => x.Name == retainerName); + if (retainer == null) + { + PluginLog.Information("No retainer information found"); + } + else if (!retainer.Managed) + { + PluginLog.Information("Retainer is not managed"); + } + 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 = _gameCache.Ventures + .Where(x => retainer.Level >= x.Level) + .FirstOrDefault(x => x.ItemId == queuedItem.ItemId && x.MatchesJob(retainer.Job)); + if (venture == null) + { + PluginLog.Information($"No applicable venture found for itemId {queuedItem.ItemId}"); + } + else + { + var itemToGather = _gameCache.ItemsToGather.FirstOrDefault(x => x.ItemId == queuedItem.ItemId); + if (itemToGather != null && !ch.GatheredItems.Contains(itemToGather.GatheredItemId)) + { + PluginLog.Information($"Character hasn't gathered {venture.Name} yet"); + } + else + { + PluginLog.Information( + $"Found venture {venture.Name}, row = {venture.RowId}, checking if it is suitable"); + VentureReward? reward = null; + if (venture.CategoryName is "MIN" or "BTN") + { + if (retainer.Gathering >= venture.RequiredGathering) + reward = venture.Rewards.Last( + x => retainer.Perception >= x.PerceptionMinerBotanist); + } + else if (venture.CategoryName == "FSH") + { + if (retainer.Gathering >= venture.RequiredGathering) + reward = venture.Rewards.Last( + x => retainer.Perception >= x.PerceptionFisher); + } + else + { + if (retainer.ItemLevel >= venture.ItemLevelCombat) + reward = venture.Rewards.Last( + x => retainer.ItemLevel >= x.ItemLevelCombat); + } + + if (reward == null) + { + PluginLog.Information( + "Retainer doesn't have enough stats for the venture and would return no items"); + } + else + { + _chatGui.Print( + $"ARC → Overriding venture 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.RemainingQuantity = + Math.Max(0, queuedItem.RemainingQuantity - reward.Quantity); + _pluginInterface.SavePluginConfig(_configuration); + return; + } + } + } + } + + // fallback: managed but no venture found + if (retainer.LastVenture != 395) + { + _chatGui.Print("ARC → No tasks left, using QC"); + PluginLog.Information($"No tasks left (previous venture = {retainer.LastVenture}), using QC"); + _autoRetainerApi.SetVenture(395); + } + else + PluginLog.Information("Not changing venture plan, already 395"); + } + } + } + + private void RetainerTaskButtonDraw(ulong characterId, string retainerName) + { + Configuration.CharacterConfiguration? characterConfiguration = _configuration.Characters.FirstOrDefault(x => x.LocalContentId == characterId); + if (characterConfiguration is not { Managed: true }) + return; + + Configuration.RetainerConfiguration? retainer = characterConfiguration.Retainers.FirstOrDefault(x => x.Name == retainerName); + if (retainer is not { Managed: true }) + return; + + ImGui.SameLine(); + ImGuiComponents.IconButton(FontAwesomeIcon.Book); + } + + private void TerritoryChanged(object? sender, ushort e) => Sync(); + + public void Sync() + { + bool save = false; + + // FIXME This should have a way to get blacklisted character ids + foreach (ulong registeredCharacterId in _autoRetainerApi.GetRegisteredCharacters()) + { + PluginLog.Information($"ch → {registeredCharacterId:X}"); + var offlineCharacterData = _autoRetainerApi.GetOfflineCharacterData(registeredCharacterId); + if (offlineCharacterData.ExcludeRetainer) + continue; + + var character = _configuration.Characters.SingleOrDefault(x => x.LocalContentId == registeredCharacterId); + if (character == null) + { + character = new Configuration.CharacterConfiguration + { + LocalContentId = registeredCharacterId, + CharacterName = offlineCharacterData.Name, + WorldName = offlineCharacterData.World, + Managed = false, + }; + + save = true; + _configuration.Characters.Add(character); + } + + if (character.GatheredItems != offlineCharacterData.UnlockedGatheringItems) + { + character.GatheredItems = offlineCharacterData.UnlockedGatheringItems; + save = true; + } + + foreach (var retainerData in offlineCharacterData.RetainerData) + { + var retainer = character.Retainers.SingleOrDefault(x => x.Name == retainerData.Name); + if (retainer == null) + { + retainer = new Configuration.RetainerConfiguration + { + Name = retainerData.Name, + Managed = false, + }; + + save = true; + character.Retainers.Add(retainer); + } + + if (retainer.DisplayOrder != retainerData.DisplayOrder) + { + retainer.DisplayOrder = retainerData.DisplayOrder; + save = true; + } + + if (retainer.Level != retainerData.Level) + { + retainer.Level = retainerData.Level; + save = true; + } + + if (retainer.Job != retainerData.Job) + { + retainer.Job = retainerData.Job; + save = true; + } + + if (retainer.LastVenture != retainerData.VentureID) + { + retainer.LastVenture = retainerData.VentureID; + save = true; + } + + var additionalData = + _autoRetainerApi.GetAdditionalRetainerData(registeredCharacterId, retainerData.Name); + if (retainer.ItemLevel != additionalData.Ilvl) + { + retainer.ItemLevel = additionalData.Ilvl; + save = true; + } + + if (retainer.Gathering != additionalData.Gathering) + { + retainer.Gathering = additionalData.Gathering; + save = true; + } + + if (retainer.Perception != additionalData.Perception) + { + retainer.Perception = additionalData.Perception; + save = true; + } + } + } + + if (save) + _pluginInterface.SavePluginConfig(_configuration); + } + + private void ProcessCommand(string command, string arguments) + { + if (arguments == "sync") + Sync(); + 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; + + _autoRetainerApi.Dispose(); + ECommonsMain.Dispose(); + + } +} diff --git a/ARControl/Configuration.cs b/ARControl/Configuration.cs new file mode 100644 index 0000000..77f1797 --- /dev/null +++ b/ARControl/Configuration.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Dalamud.Configuration; + +namespace ARControl; + +internal sealed class Configuration : IPluginConfiguration +{ + public int Version { get; set; } = 1; + + public List QueuedItems { get; set; } = new(); + public List Characters { get; set; } = new(); + + public sealed class QueuedItem + { + public required uint ItemId { get; set; } + public required int RemainingQuantity { get; set; } + } + + 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 List Retainers { get; set; } = new(); + public HashSet GatheredItems { get; set; } = new(); + + public override string ToString() => $"{CharacterName} @ {WorldName}"; + } + + public sealed class RetainerConfiguration + { + public required string Name { get; set; } + public required bool Managed { get; set; } + public int DisplayOrder { get; set; } + public int Level { get; set; } + public uint Job { get; set; } + public uint LastVenture { get; set; } + public int ItemLevel { get; set; } + public int Gathering { get; set; } + public int Perception { get; set; } + } +} diff --git a/ARControl/DalamudPackager.targets b/ARControl/DalamudPackager.targets new file mode 100644 index 0000000..44e44a6 --- /dev/null +++ b/ARControl/DalamudPackager.targets @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/ARControl/GameData/GameCache.cs b/ARControl/GameData/GameCache.cs new file mode 100644 index 0000000..f375782 --- /dev/null +++ b/ARControl/GameData/GameCache.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace ARControl.GameData; + +internal sealed class GameCache +{ + public GameCache(DataManager dataManager) + { + Jobs = dataManager.GetExcelSheet()!.ToDictionary(x => x.RowId, x => x.Abbreviation.ToString()); + Ventures = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0 && !x.IsRandom && x.Task != 0) + .Select(x => new Venture(dataManager, x)) + .ToList() + .AsReadOnly(); + ItemsToGather = dataManager.GetExcelSheet()! + .Where(x => x.RowId > 0 && x.RowId < 10_000 && x.Item != 0 && x.Quest.Row == 0) + .Where(x => Ventures.Any(y => y.ItemId == x.Item)) + .Select(x => new ItemToGather(dataManager, x)) + .OrderBy(x => x.Name) + .ToList() + .AsReadOnly(); + } + + public IReadOnlyDictionary Jobs { get; } + public IReadOnlyList Ventures { get; } + public IReadOnlyList ItemsToGather { get; } +} diff --git a/ARControl/GameData/ItemToGather.cs b/ARControl/GameData/ItemToGather.cs new file mode 100644 index 0000000..1297e27 --- /dev/null +++ b/ARControl/GameData/ItemToGather.cs @@ -0,0 +1,19 @@ +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace ARControl.GameData; + +internal sealed class ItemToGather +{ + public ItemToGather(DataManager dataManager, GatheringItem item) + { + GatheredItemId = item.RowId; + ItemId = item.Item; + Name = dataManager.GetExcelSheet()!.GetRow((uint)item.Item)!.Name.ToString(); + } + + + public uint GatheredItemId { get; } + public int ItemId { get; } + public string Name { get; } +} diff --git a/ARControl/GameData/Venture.cs b/ARControl/GameData/Venture.cs new file mode 100644 index 0000000..7258f22 --- /dev/null +++ b/ARControl/GameData/Venture.cs @@ -0,0 +1,93 @@ +using System.Collections.Generic; +using Dalamud.Data; +using Lumina.Excel.GeneratedSheets; + +namespace ARControl.GameData; + +internal sealed class Venture +{ + public Venture(DataManager dataManager, RetainerTask retainerTask) + { + RowId = retainerTask.RowId; + Category = retainerTask.ClassJobCategory.Value!; + + var taskDetails = dataManager.GetExcelSheet()!.GetRow(retainerTask.Task)!; + var taskParameters = retainerTask.RetainerTaskParameter.Value!; + ItemId = taskDetails.Item.Row; + Name = taskDetails.Item.Value!.Name.ToString(); + Level = retainerTask.RetainerLevel; + ItemLevelCombat = retainerTask.RequiredItemLevel; + RequiredGathering = retainerTask.RequiredGathering; + Rewards = new List + { + new VentureReward + { + Quantity = taskDetails.Quantity[0], + ItemLevelCombat = 0, + PerceptionMinerBotanist = 0, + PerceptionFisher = 0, + }, + new VentureReward + { + Quantity = taskDetails.Quantity[1], + ItemLevelCombat = taskParameters.ItemLevelDoW[0], + PerceptionMinerBotanist = taskParameters.PerceptionDoL[0], + PerceptionFisher = taskParameters.PerceptionFSH[0], + }, + new VentureReward + { + Quantity = taskDetails.Quantity[2], + ItemLevelCombat = taskParameters.ItemLevelDoW[1], + PerceptionMinerBotanist = taskParameters.PerceptionDoL[1], + PerceptionFisher = taskParameters.PerceptionFSH[1], + }, + new VentureReward + { + Quantity = taskDetails.Quantity[3], + ItemLevelCombat = taskParameters.ItemLevelDoW[2], + PerceptionMinerBotanist = taskParameters.PerceptionDoL[2], + PerceptionFisher = taskParameters.PerceptionFSH[2], + }, + new VentureReward + { + Quantity = taskDetails.Quantity[4], + ItemLevelCombat = taskParameters.ItemLevelDoW[3], + PerceptionMinerBotanist = taskParameters.PerceptionDoL[3], + PerceptionFisher = taskParameters.PerceptionFSH[3], + } + }; + } + + public uint RowId { get; } + public ClassJobCategory Category { get; } + + public string? CategoryName + { + get + { + return Category.RowId switch + { + 17 => "MIN", + 18 => "BTN", + 19 => "FSH", + _ => "DoWM", + }; + } + } + + public uint ItemId { get; } + public string Name { get; } + public byte Level { get; } + public ushort ItemLevelCombat { get; } + public ushort RequiredGathering { get; set; } + + public List Rewards { get; } + + public bool MatchesJob(uint job) + { + if (Category.RowId >= 17 && Category.RowId <= 19) + return Category.RowId == job + 1; + else + return job is < 16 or > 18; + } +} diff --git a/ARControl/GameData/VentureReward.cs b/ARControl/GameData/VentureReward.cs new file mode 100644 index 0000000..cfc8a4d --- /dev/null +++ b/ARControl/GameData/VentureReward.cs @@ -0,0 +1,9 @@ +namespace ARControl.GameData; + +internal sealed class VentureReward +{ + public required byte Quantity { get; init; } + public required short ItemLevelCombat { get; init; } + public required short PerceptionMinerBotanist { get; init; } + public required short PerceptionFisher { get; init; } +} diff --git a/ARControl/Windows/ConfigWindow.cs b/ARControl/Windows/ConfigWindow.cs new file mode 100644 index 0000000..8e7a55d --- /dev/null +++ b/ARControl/Windows/ConfigWindow.cs @@ -0,0 +1,408 @@ +using System.Collections.Generic; +using System.Linq; +using System.Numerics; +using ARControl.GameData; +using Dalamud.Game.ClientState; +using Dalamud.Game.Command; +using Dalamud.Interface; +using Dalamud.Interface.Colors; +using Dalamud.Interface.Components; +using Dalamud.Interface.Style; +using Dalamud.Interface.Windowing; +using Dalamud.Logging; +using Dalamud.Plugin; +using ECommons.ImGuiMethods; +using ImGuiNET; + +namespace ARControl.Windows; + +internal sealed class ConfigWindow : Window +{ + private const byte MaxLevel = 90; + + private static readonly Vector4 ColorGreen = ImGuiColors.HealerGreen; + private static readonly Vector4 ColorRed = ImGuiColors.DalamudRed; + private static readonly Vector4 ColorGrey = ImGuiColors.DalamudGrey; + + private readonly DalamudPluginInterface _pluginInterface; + private readonly Configuration _configuration; + private readonly GameCache _gameCache; + private readonly ClientState _clientState; + private readonly CommandManager _commandManager; + + private string _searchString = string.Empty; + private Configuration.QueuedItem? _dragDropSource; + private bool _enableDragDrop; + private bool _checkPerCharacter = true; + private bool _onlyShowMissing = true; + + public ConfigWindow( + DalamudPluginInterface pluginInterface, + Configuration configuration, + GameCache gameCache, + ClientState clientState, + CommandManager commandManager) + : base("ARC###ARControlConfig") + { + _pluginInterface = pluginInterface; + _configuration = configuration; + _gameCache = gameCache; + _clientState = clientState; + _commandManager = commandManager; + } + + public override void Draw() + { + if (ImGui.BeginTabBar("ARConfigTabs")) + { + DrawItemQueue(); + DrawCharacters(); + DrawGatheredItemsToCheck(); + ImGui.EndTabBar(); + } + } + + private unsafe void DrawItemQueue() + { + if (ImGui.BeginTabItem("Venture Queue")) + { + if (ImGui.BeginCombo("Venture...##VentureSelection", "")) + { + ImGuiEx.SetNextItemFullWidth(); + ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256); + + foreach (var ventures in _gameCache.Ventures + .Where(x => x.Name.ToLower().Contains(_searchString.ToLower())) + .OrderBy(x => x.Level) + .ThenBy(x => x.Name) + .ThenBy(x => x.ItemId) + .GroupBy(x => x.ItemId)) + { + var venture = ventures.First(); + if (ImGui.Selectable( + $"{venture.Name} ({string.Join(" ", ventures.Select(x => x.CategoryName))})##SelectVenture{venture.RowId}")) + { + _configuration.QueuedItems.Add(new Configuration.QueuedItem + { + ItemId = venture.ItemId, + RemainingQuantity = 0, + }); + _searchString = string.Empty; + Save(); + } + } + + ImGui.EndCombo(); + } + + ImGui.Checkbox("Enable Drag&Drop", ref _enableDragDrop); + ImGui.Separator(); + + ImGui.Indent(30); + + Configuration.QueuedItem? itemToRemove = null; + Configuration.QueuedItem? itemToAdd = null; + int indexToAdd = 0; + for (int i = 0; i < _configuration.QueuedItems.Count; ++i) + { + 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) + { + 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.BeginPopup($"###ctx{i}")) + { + if (ImGui.Selectable($"Remove {venture.Name}")) + itemToRemove = item; + + ImGui.EndPopup(); + } + + ImGui.PopID(); + } + + if (itemToRemove != null) + { + _configuration.QueuedItems.Remove(itemToRemove); + Save(); + } + + if (itemToAdd != null) + { + PluginLog.Information($"Updating {itemToAdd.ItemId} → {indexToAdd}"); + _configuration.QueuedItems.Remove(itemToAdd); + _configuration.QueuedItems.Insert(indexToAdd, itemToAdd); + Save(); + } + + ImGui.Unindent(30); + ImGui.EndTabItem(); + } + } + + private void DrawCharacters() + { + if (ImGui.BeginTabItem("Retainers")) + { + foreach (var world in _configuration.Characters + .Where(x => x.Retainers.Any(y => y.Job != 0)) + .OrderBy(x => x.LocalContentId) + .GroupBy(x => x.WorldName)) + { + ImGui.CollapsingHeader(world.Key, ImGuiTreeNodeFlags.DefaultOpen | ImGuiTreeNodeFlags.OpenOnArrow | ImGuiTreeNodeFlags.Bullet); + foreach (var character in world) + { + ImGui.PushID($"Char{character.LocalContentId}"); + + ImGui.PushItemWidth(ImGui.GetFontSize() * 30); + Vector4 buttonColor = new Vector4(); + if (character.Managed && character.Retainers.Count > 0) + { + if (character.Retainers.All(x => x.Managed)) + buttonColor = ImGuiColors.HealerGreen; + else if (character.Retainers.All(x => !x.Managed)) + buttonColor = ImGuiColors.DalamudRed; + else + buttonColor = ImGuiColors.DalamudOrange; + } + + if (ImGuiComponents.IconButton(FontAwesomeIcon.Book, buttonColor)) + { + character.Managed = !character.Managed; + Save(); + } + + ImGui.SameLine(); + + if (ImGui.CollapsingHeader( + $"{character.CharacterName} {(character.Managed ? $"({character.Retainers.Count(x => x.Managed)} / {character.Retainers.Count})" : "")}###{character.LocalContentId}")) + { + ImGui.Indent(30); + foreach (var retainer in character.Retainers.Where(x => x.Job > 0).OrderBy(x => x.DisplayOrder)) + { + ImGui.BeginDisabled(retainer.Level < MaxLevel); + + bool managed = retainer.Managed && retainer.Level == MaxLevel; + ImGui.Text(_gameCache.Jobs[retainer.Job]); + ImGui.SameLine(); + if (ImGui.Checkbox($"{retainer.Name}###Retainer{retainer.Name}{retainer.DisplayOrder}", + ref managed)) + { + retainer.Managed = managed; + Save(); + } + + ImGui.EndDisabled(); + } + + ImGui.Unindent(30); + } + + ImGui.PopID(); + } + } + + ImGui.EndTabItem(); + } + } + + private void DrawGatheredItemsToCheck() + { + if (ImGui.BeginTabItem("Locked Items")) + { + ImGui.Checkbox("Group by character", ref _checkPerCharacter); + ImGui.Checkbox("Only show missing items", ref _onlyShowMissing); + ImGui.Separator(); + + var itemsToCheck = + _configuration.QueuedItems + .Select(x => x.ItemId) + .Distinct() + .Select(itemId => new + { + GatheredItem = _gameCache.ItemsToGather.SingleOrDefault(x => x.ItemId == itemId), + Ventures = _gameCache.Ventures.Where(x => x.ItemId == itemId).ToList() + }) + .Where(x => x.GatheredItem != null && x.Ventures.Count > 0) + .Select(x => new CheckedItem + { + GatheredItem = x.GatheredItem!, + Ventures = x.Ventures, + ItemId = x.Ventures.First().ItemId, + }) + .ToList(); + + var charactersToCheck = _configuration.Characters + .Where(x => x.Managed) + .OrderBy(x => x.WorldName) + .ThenBy(x => x.LocalContentId) + .Select(x => new CheckedCharacter(x, itemsToCheck)) + .ToList(); + + if (_checkPerCharacter) + { + foreach (var ch in charactersToCheck.Where(x => x.ToCheck(_onlyShowMissing).Any())) + { + bool currentCharacter = _clientState.LocalContentId == ch.Character.LocalContentId; + ImGui.BeginDisabled(currentCharacter); + if (ImGuiComponents.IconButton($"SwitchChacters{ch.Character.LocalContentId}", + FontAwesomeIcon.DoorOpen)) + { + _commandManager.ProcessCommand($"/ays relog {ch.Character.CharacterName}@{ch.Character.WorldName}"); + } + + ImGui.EndDisabled(); + ImGui.SameLine(); + + if (currentCharacter) + ImGui.PushStyleColor(ImGuiCol.Text, ImGuiColors.HealerGreen); + + bool expanded = ImGui.CollapsingHeader($"{ch.Character}###GatheredCh{ch.Character.LocalContentId}"); + if (currentCharacter) + ImGui.PopStyleColor(); + + if (expanded) + { + ImGui.Indent(30); + foreach (var item in itemsToCheck.Where(x => + ch.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId))) + { + var color = ch.Items[item.ItemId]; + if (color != ColorGrey) + { + ImGui.PushStyleColor(ImGuiCol.Text, color); + if (currentCharacter && color == ColorRed) + { + ImGui.Selectable(item.GatheredItem.Name); + if (ImGui.IsItemClicked(ImGuiMouseButton.Left)) + { + uint classJob = _clientState.LocalPlayer!.ClassJob.Id; + if (classJob == 16) + _commandManager.ProcessCommand($"/gathermin {item.GatheredItem.Name}"); + else if (classJob == 17) + _commandManager.ProcessCommand($"/gatherbtn {item.GatheredItem.Name}"); + else if (classJob == 18) + _commandManager.ProcessCommand($"/gatherfish {item.GatheredItem.Name}"); + else + _commandManager.ProcessCommand($"/gather {item.GatheredItem.Name}"); + } + } + else + { + ImGui.Text(item.GatheredItem.Name); + } + ImGui.PopStyleColor(); + } + } + + ImGui.Unindent(30); + } + } + } + else + { + foreach (var item in itemsToCheck.Where(x => + charactersToCheck.Any(y => y.ToCheck(_onlyShowMissing).ContainsKey(x.ItemId)))) + { + if (ImGui.CollapsingHeader($"{item.GatheredItem.Name}##Gathered{item.GatheredItem.ItemId}")) + { + ImGui.Indent(30); + foreach (var ch in charactersToCheck) + { + var color = ch.Items[item.ItemId]; + if (color == ColorRed || (color == ColorGreen && !_onlyShowMissing)) + ImGui.TextColored(color, ch.Character.ToString()); + } + + ImGui.Unindent(30); + } + } + } + + ImGui.EndTabItem(); + } + } + + private void Save() + { + _pluginInterface.SavePluginConfig(_configuration); + } + + private sealed class CheckedCharacter + { + public CheckedCharacter(Configuration.CharacterConfiguration character, + List itemsToCheck) + { + Character = character; + + foreach (var item in itemsToCheck) + { + bool enabled = character.Retainers.Any(x => item.Ventures.Any(v => v.MatchesJob(x.Job))); + if (enabled) + { + if (character.GatheredItems.Contains(item.GatheredItem.GatheredItemId)) + Items[item.ItemId] = ColorGreen; + else + Items[item.ItemId] = ColorRed; + } + else + Items[item.ItemId] = ColorGrey; + } + } + + public Configuration.CharacterConfiguration Character { get; } + public Dictionary Items { get; } = new(); + + public Dictionary 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 Ventures { get; init; } + public required uint ItemId { get; init; } + } +} diff --git a/ARControl/packages.lock.json b/ARControl/packages.lock.json new file mode 100644 index 0000000..6bf9223 --- /dev/null +++ b/ARControl/packages.lock.json @@ -0,0 +1,19 @@ +{ + "version": 1, + "dependencies": { + "net7.0-windows7.0": { + "Dalamud.ContextMenu": { + "type": "Direct", + "requested": "[1.2.3, )", + "resolved": "1.2.3", + "contentHash": "ydemplF7DNcA/LLeongDVzWUD/JV0Fw3EwA2+P0jYq3Le2ZYSt4U8qyJq4FyoChqt0lFG8BxYCAzfeWp4Jmnqw==" + }, + "DalamudPackager": { + "type": "Direct", + "requested": "[2.1.11, )", + "resolved": "2.1.11", + "contentHash": "9qlAWoRRTiL/geAvuwR/g6Bcbrd/bJJgVnB/RurBiyKs6srsP0bvpoo8IK+Eg8EA6jWeM6/YJWs66w4FIAzqPw==" + } + } + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..05af0a8 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# ARC + +Instead of manually managing retainer plans for each individual character, +which can get fairly tedious fairly quickly, we optimize this a bit and +only manage "high-level" plans. + +This means we create a list a la: + +- 5000 Cobalt Ore +- 2000 Gold Ore + +... and ARC distributes each venture automatically amongst all characters +which are included. diff --git a/global.json b/global.json new file mode 100644 index 0000000..aaac9e0 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file