Restructure config window
authorLiza Carvelli <liza@carvel.li>
Thu, 20 Feb 2025 20:16:48 +0000 (21:16 +0100)
committerLiza Carvelli <liza@carvel.li>
Thu, 20 Feb 2025 20:16:48 +0000 (21:16 +0100)
Questionable/QuestionablePlugin.cs
Questionable/Windows/ConfigComponents/ConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigComponents/DebugConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigComponents/DutyConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs [new file with mode: 0644]
Questionable/Windows/ConfigWindow.cs

index ccd694af46f2decb800c1117e66efa6603d7c6e5..0f776eb62e798de585ccde41becd9a65d981d004 100644 (file)
@@ -26,6 +26,7 @@ using Questionable.Functions;
 using Questionable.Validation;
 using Questionable.Validation.Validators;
 using Questionable.Windows;
+using Questionable.Windows.ConfigComponents;
 using Questionable.Windows.JournalComponents;
 using Questionable.Windows.QuestComponents;
 using Action = Questionable.Controller.Steps.Interactions.Action;
@@ -298,6 +299,11 @@ public sealed class QuestionablePlugin : IDalamudPlugin
         serviceCollection.AddSingleton<QuestValidationWindow>();
         serviceCollection.AddSingleton<JournalProgressWindow>();
         serviceCollection.AddSingleton<PriorityWindow>();
+
+        serviceCollection.AddSingleton<GeneralConfigComponent>();
+        serviceCollection.AddSingleton<DutyConfigComponent>();
+        serviceCollection.AddSingleton<NotificationConfigComponent>();
+        serviceCollection.AddSingleton<DebugConfigComponent>();
     }
 
     private static void AddQuestValidators(ServiceCollection serviceCollection)
diff --git a/Questionable/Windows/ConfigComponents/ConfigComponent.cs b/Questionable/Windows/ConfigComponents/ConfigComponent.cs
new file mode 100644 (file)
index 0000000..0a5be62
--- /dev/null
@@ -0,0 +1,64 @@
+using System.Text;
+using Dalamud.Game.Text;
+using Dalamud.Plugin;
+using ImGuiNET;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal abstract class ConfigComponent
+{
+    protected const string DutyClipboardSeparator = ";";
+    protected const string DutyWhitelistPrefix = "+";
+    protected const string DutyBlacklistPrefix = "-";
+
+    protected readonly string[] SupportedCfcOptions =
+    [
+        $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)",
+        $"{SeIconChar.Circle.ToIconChar()} Enabled",
+        $"{SeIconChar.Cross.ToIconChar()} Disabled"
+    ];
+
+    protected readonly string[] UnsupportedCfcOptions =
+    [
+        $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)",
+        $"{SeIconChar.Circle.ToIconChar()} Enabled",
+        $"{SeIconChar.Cross.ToIconChar()} Disabled"
+    ];
+
+    private readonly IDalamudPluginInterface _pluginInterface;
+
+    protected ConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration)
+    {
+        _pluginInterface = pluginInterface;
+        Configuration = configuration;
+    }
+
+    protected Configuration Configuration { get; }
+
+    public abstract void DrawTab();
+
+    protected void Save() => _pluginInterface.SavePluginConfig(Configuration);
+
+    protected static string FormatLevel(int level)
+    {
+        if (level == 0)
+            return string.Empty;
+
+        return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
+    }
+
+    /// <summary>
+    /// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
+    /// </summary>
+    protected unsafe string? GetClipboardText()
+    {
+        byte* ptr = ImGuiNative.igGetClipboardText();
+        if (ptr == null)
+            return null;
+
+        int byteCount = 0;
+        while (ptr[byteCount] != 0)
+            ++byteCount;
+        return Encoding.UTF8.GetString(ptr, byteCount);
+    }
+}
diff --git a/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs
new file mode 100644 (file)
index 0000000..7d89efd
--- /dev/null
@@ -0,0 +1,49 @@
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using ImGuiNET;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal sealed class DebugConfigComponent : ConfigComponent
+{
+    public DebugConfigComponent(IDalamudPluginInterface pluginInterface, Configuration configuration)
+        : base(pluginInterface, configuration)
+    {
+    }
+
+    public override void DrawTab()
+    {
+        using var tab = ImRaii.TabItem("Advanced");
+        if (!tab)
+            return;
+
+        ImGui.TextColored(ImGuiColors.DalamudRed,
+            "Enabling any option here may cause unexpected behavior. Use at your own risk.");
+
+        ImGui.Separator();
+
+        bool debugOverlay = Configuration.Advanced.DebugOverlay;
+        if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay))
+        {
+            Configuration.Advanced.DebugOverlay = debugOverlay;
+            Save();
+        }
+
+        bool neverFly = Configuration.Advanced.NeverFly;
+        if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly))
+        {
+            Configuration.Advanced.NeverFly = neverFly;
+            Save();
+        }
+
+        bool additionalStatusInformation = Configuration.Advanced.AdditionalStatusInformation;
+        if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation))
+        {
+            Configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation;
+            Save();
+        }
+
+        ImGui.EndTabItem();
+    }
+}
diff --git a/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs
new file mode 100644 (file)
index 0000000..ffb6538
--- /dev/null
@@ -0,0 +1,249 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using System.Numerics;
+using System.Text;
+using Dalamud.Game.Text;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using ImGuiNET;
+using Lumina.Excel.Sheets;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.External;
+using Questionable.Model;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal sealed class DutyConfigComponent : ConfigComponent
+{
+    private const string DutyClipboardPrefix = "qst:duty:";
+
+    private readonly QuestRegistry _questRegistry;
+    private readonly AutoDutyIpc _autoDutyIpc;
+    private readonly Dictionary<EExpansionVersion, List<DutyInfo>> _contentFinderConditionNames;
+
+    public DutyConfigComponent(
+        IDalamudPluginInterface pluginInterface,
+        Configuration configuration,
+        IDataManager dataManager,
+        QuestRegistry questRegistry,
+        AutoDutyIpc autoDutyIpc,
+        TerritoryData territoryData)
+        : base(pluginInterface, configuration)
+    {
+        _questRegistry = questRegistry;
+        _autoDutyIpc = autoDutyIpc;
+
+        _contentFinderConditionNames = dataManager.GetExcelSheet<DawnContent>()
+            .Where(x => x is { RowId: > 0, Unknown16: false })
+            .OrderBy(x => x.Unknown15) // SortKey for the support UI
+            .Select(x => x.Content.ValueNullable)
+            .Where(x => x != null)
+            .Select(x => x!.Value)
+            .Select(x => new
+            {
+                Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId,
+                CfcId = x.RowId,
+                Name = territoryData.GetContentFinderCondition(x.RowId)?.Name ?? "?",
+                TerritoryId = x.TerritoryType.RowId,
+                ContentType = x.ContentType.RowId,
+                Level = x.ClassJobLevelRequired,
+                x.SortKey
+            })
+            .GroupBy(x => x.Expansion)
+            .ToDictionary(x => x.Key,
+                x => x
+                    .Select(y => new DutyInfo(y.CfcId, y.TerritoryId,
+                        $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}"))
+                    .ToList());
+    }
+
+    public override void DrawTab()
+    {
+        using var tab = ImRaii.TabItem("Duties");
+        if (!tab)
+            return;
+
+        bool runInstancedContentWithAutoDuty = Configuration.Duties.RunInstancedContentWithAutoDuty;
+        if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty))
+        {
+            Configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty;
+            Save();
+        }
+
+        ImGui.SameLine();
+        ImGuiComponents.HelpMarker(
+            "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration.");
+
+        ImGui.Separator();
+
+        using (ImRaii.Disabled(!runInstancedContentWithAutoDuty))
+        {
+            ImGui.Text(
+                "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed.");
+
+            ImGui.Text(
+                "The included list of duties can change with each update, and is based on the following spreadsheet:");
+            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet"))
+                Util.OpenLink(
+                    "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0");
+
+            ImGui.Separator();
+            ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:");
+
+            DrawConfigTable(runInstancedContentWithAutoDuty);
+            DrawClipboardButtons();
+
+            ImGui.SameLine();
+
+            using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
+            {
+                if (ImGui.Button("Reset to default"))
+                {
+                    Configuration.Duties.WhitelistedDutyCfcIds.Clear();
+                    Configuration.Duties.BlacklistedDutyCfcIds.Clear();
+                    Save();
+                }
+            }
+
+            if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
+                ImGui.SetTooltip("Hold CTRL to enable this button.");
+        }
+    }
+
+    private void DrawConfigTable(bool runInstancedContentWithAutoDuty)
+    {
+        using var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true);
+        if (!child)
+            return;
+
+        foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
+        {
+            if (ImGui.CollapsingHeader(expansion.ToString()))
+            {
+                using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit);
+                if (table)
+                {
+                    ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
+                    ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f);
+
+                    if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames))
+                    {
+                        foreach (var (cfcId, territoryId, name) in cfcNames)
+                        {
+                            if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId,
+                                    out bool autoDutyEnabledByDefault))
+                            {
+                                ImGui.TableNextRow();
+
+                                string[] labels = autoDutyEnabledByDefault
+                                    ? SupportedCfcOptions
+                                    : UnsupportedCfcOptions;
+                                int value = 0;
+                                if (Configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId))
+                                    value = 1;
+                                if (Configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId))
+                                    value = 2;
+
+                                if (ImGui.TableNextColumn())
+                                {
+                                    ImGui.AlignTextToFramePadding();
+                                    ImGui.TextUnformatted(name);
+                                    if (ImGui.IsItemHovered() &&
+                                        Configuration.Advanced.AdditionalStatusInformation)
+                                    {
+                                        using var tooltip = ImRaii.Tooltip();
+                                        if (tooltip)
+                                        {
+                                            ImGui.TextUnformatted(name);
+                                            ImGui.Separator();
+                                            ImGui.BulletText($"TerritoryId: {territoryId}");
+                                            ImGui.BulletText($"ContentFinderConditionId: {cfcId}");
+                                        }
+                                    }
+
+                                    if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId))
+                                        ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty",
+                                            FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
+                                }
+
+                                if (ImGui.TableNextColumn())
+                                {
+                                    using var _ = ImRaii.PushId($"##Dungeon{cfcId}");
+                                    ImGui.SetNextItemWidth(200);
+                                    if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
+                                    {
+                                        Configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId);
+                                        Configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId);
+
+                                        if (value == 1)
+                                            Configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId);
+                                        else if (value == 2)
+                                            Configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId);
+
+                                        Save();
+                                    }
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private void DrawClipboardButtons()
+    {
+        using (ImRaii.Disabled(Configuration.Duties.WhitelistedDutyCfcIds.Count +
+                   Configuration.Duties.BlacklistedDutyCfcIds.Count == 0))
+        {
+            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard"))
+            {
+                var whitelisted =
+                    Configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}");
+                var blacklisted =
+                    Configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}");
+                string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(
+                    string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted))));
+                ImGui.SetClipboardText(text);
+            }
+        }
+
+        ImGui.SameLine();
+
+        string? clipboardText = GetClipboardText();
+        using (ImRaii.Disabled(clipboardText == null ||
+                               !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture)))
+        {
+            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard"))
+            {
+                clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length);
+                string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText));
+
+                Configuration.Duties.WhitelistedDutyCfcIds.Clear();
+                Configuration.Duties.BlacklistedDutyCfcIds.Clear();
+                foreach (string part in text.Split(DutyClipboardSeparator))
+                {
+                    if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) &&
+                        uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture,
+                            out uint whitelistedCfcId))
+                        Configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId);
+
+                    if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) &&
+                        uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture,
+                            out uint blacklistedCfcId))
+                        Configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId);
+                }
+            }
+        }
+    }
+
+    private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
+}
diff --git a/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs
new file mode 100644 (file)
index 0000000..e58ce98
--- /dev/null
@@ -0,0 +1,114 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using ImGuiNET;
+using Lumina.Excel.Sheets;
+using Questionable.Controller;
+using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal sealed class GeneralConfigComponent : ConfigComponent
+{
+    private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
+
+    private readonly CombatController _combatController;
+
+    private readonly uint[] _mountIds;
+    private readonly string[] _mountNames;
+    private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"];
+
+    private readonly string[] _grandCompanyNames =
+        ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
+
+    public GeneralConfigComponent(
+        IDalamudPluginInterface pluginInterface,
+        Configuration configuration,
+        CombatController combatController,
+        IDataManager dataManager)
+        : base(pluginInterface, configuration)
+    {
+        _combatController = combatController;
+
+        var mounts = dataManager.GetExcelSheet<Mount>()
+            .Where(x => x is { RowId: > 0, Icon: > 0 })
+            .Select(x => (MountId: x.RowId, Name: x.Singular.ToString()))
+            .Where(x => !string.IsNullOrEmpty(x.Name))
+            .OrderBy(x => x.Name)
+            .ToList();
+        _mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray();
+        _mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray();
+    }
+
+    public override void DrawTab()
+    {
+        using var tab = ImRaii.TabItem("General");
+        if (!tab)
+            return;
+
+        using (ImRaii.Disabled(_combatController.IsRunning))
+        {
+            int selectedCombatModule = (int)Configuration.General.CombatModule;
+            if (ImGui.Combo("Preferred Combat Module", ref selectedCombatModule, _combatModuleNames,
+                    _combatModuleNames.Length))
+            {
+                Configuration.General.CombatModule = (Configuration.ECombatModule)selectedCombatModule;
+                Save();
+            }
+        }
+
+        int selectedMount = Array.FindIndex(_mountIds, x => x == Configuration.General.MountId);
+        if (selectedMount == -1)
+        {
+            selectedMount = 0;
+            Configuration.General.MountId = _mountIds[selectedMount];
+            Save();
+        }
+
+        if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length))
+        {
+            Configuration.General.MountId = _mountIds[selectedMount];
+            Save();
+        }
+
+        int grandCompany = (int)Configuration.General.GrandCompany;
+        if (ImGui.Combo("Preferred Grand Company", ref grandCompany, _grandCompanyNames,
+                _grandCompanyNames.Length))
+        {
+            Configuration.General.GrandCompany = (GrandCompany)grandCompany;
+            Save();
+        }
+
+        bool hideInAllInstances = Configuration.General.HideInAllInstances;
+        if (ImGui.Checkbox("Hide quest window in all instanced duties", ref hideInAllInstances))
+        {
+            Configuration.General.HideInAllInstances = hideInAllInstances;
+            Save();
+        }
+
+        bool useEscToCancelQuesting = Configuration.General.UseEscToCancelQuesting;
+        if (ImGui.Checkbox("Use ESC to cancel questing/movement", ref useEscToCancelQuesting))
+        {
+            Configuration.General.UseEscToCancelQuesting = useEscToCancelQuesting;
+            Save();
+        }
+
+        bool showIncompleteSeasonalEvents = Configuration.General.ShowIncompleteSeasonalEvents;
+        if (ImGui.Checkbox("Show details for incomplete seasonal events", ref showIncompleteSeasonalEvents))
+        {
+            Configuration.General.ShowIncompleteSeasonalEvents = showIncompleteSeasonalEvents;
+            Save();
+        }
+
+        bool configureTextAdvance = Configuration.General.ConfigureTextAdvance;
+        if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings",
+                ref configureTextAdvance))
+        {
+            Configuration.General.ConfigureTextAdvance = configureTextAdvance;
+            Save();
+        }
+    }
+}
diff --git a/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs
new file mode 100644 (file)
index 0000000..d0a4ba0
--- /dev/null
@@ -0,0 +1,80 @@
+using System;
+using System.Linq;
+using Dalamud.Game.Text;
+using Dalamud.Interface.Components;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Utility;
+using ImGuiNET;
+using Questionable.External;
+
+namespace Questionable.Windows.ConfigComponents;
+
+internal sealed class NotificationConfigComponent : ConfigComponent
+{
+    private readonly NotificationMasterIpc _notificationMasterIpc;
+
+    public NotificationConfigComponent(
+        IDalamudPluginInterface pluginInterface,
+        Configuration configuration,
+        NotificationMasterIpc notificationMasterIpc)
+        : base(pluginInterface, configuration)
+    {
+        _notificationMasterIpc = notificationMasterIpc;
+    }
+
+    public override void DrawTab()
+    {
+        using var tab = ImRaii.TabItem("Notifications");
+        if (!tab)
+            return;
+
+        bool enabled = Configuration.Notifications.Enabled;
+        if (ImGui.Checkbox("Enable notifications when manual interaction is required", ref enabled))
+        {
+            Configuration.Notifications.Enabled = enabled;
+            Save();
+        }
+
+        using (ImRaii.Disabled(!Configuration.Notifications.Enabled))
+        {
+            using (ImRaii.PushIndent())
+            {
+                var xivChatTypes = Enum.GetValues<XivChatType>()
+                    .Where(x => x != XivChatType.StandardEmote)
+                    .ToArray();
+                var selectedChatType = Array.IndexOf(xivChatTypes, Configuration.Notifications.ChatType);
+                string[] chatTypeNames = xivChatTypes
+                    .Select(t => t.GetAttribute<XivChatTypeInfoAttribute>()?.FancyName ?? t.ToString())
+                    .ToArray();
+                if (ImGui.Combo("Chat channel", ref selectedChatType, chatTypeNames,
+                        chatTypeNames.Length))
+                {
+                    Configuration.Notifications.ChatType = xivChatTypes[selectedChatType];
+                    Save();
+                }
+
+                ImGui.Separator();
+                ImGui.Text("NotificationMaster settings");
+                ImGui.SameLine();
+                ImGuiComponents.HelpMarker("Requires the plugin 'NotificationMaster' to be installed.");
+                using (ImRaii.Disabled(!_notificationMasterIpc.Enabled))
+                {
+                    bool showTrayMessage = Configuration.Notifications.ShowTrayMessage;
+                    if (ImGui.Checkbox("Show tray notification", ref showTrayMessage))
+                    {
+                        Configuration.Notifications.ShowTrayMessage = showTrayMessage;
+                        Save();
+                    }
+
+                    bool flashTaskbar = Configuration.Notifications.FlashTaskbar;
+                    if (ImGui.Checkbox("Flash taskbar icon", ref flashTaskbar))
+                    {
+                        Configuration.Notifications.FlashTaskbar = flashTaskbar;
+                        Save();
+                    }
+                }
+            }
+        }
+    }
+}
index 131f3726d6c4b130ce3e3adea26716bd0e3f1a7c..e2ac6c314997ec6f26484f8e681289d0ab02804c 100644 (file)
-using System;
-using System.Collections.Generic;
-using System.Globalization;
-using System.Linq;
-using System.Numerics;
-using System.Text;
-using Dalamud.Game.Text;
-using Dalamud.Interface;
-using Dalamud.Interface.Colors;
-using Dalamud.Interface.Components;
-using Dalamud.Interface.Utility.Raii;
+using Dalamud.Interface.Utility.Raii;
 using Dalamud.Plugin;
-using Dalamud.Plugin.Services;
-using Dalamud.Utility;
 using ImGuiNET;
 using LLib.ImGui;
-using Lumina.Excel.Sheets;
-using Questionable.Controller;
-using Questionable.Data;
-using Questionable.External;
-using Questionable.Model;
-using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
+using Questionable.Windows.ConfigComponents;
 
 namespace Questionable.Windows;
 
 internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig
 {
-    private const string DutyClipboardPrefix = "qst:duty:";
-    private const string DutyClipboardSeparator = ";";
-    private const string DutyWhitelistPrefix = "+";
-    private const string DutyBlacklistPrefix = "-";
-
-    private static readonly List<(uint Id, string Name)> DefaultMounts = [(0, "Mount Roulette")];
-
     private readonly IDalamudPluginInterface _pluginInterface;
-    private readonly NotificationMasterIpc _notificationMasterIpc;
+    private readonly GeneralConfigComponent _generalConfigComponent;
+    private readonly DutyConfigComponent _dutyConfigComponent;
+    private readonly NotificationConfigComponent _notificationConfigComponent;
+    private readonly DebugConfigComponent _debugConfigComponent;
     private readonly Configuration _configuration;
-    private readonly CombatController _combatController;
-    private readonly QuestRegistry _questRegistry;
-    private readonly AutoDutyIpc _autoDutyIpc;
-
-    private readonly uint[] _mountIds;
-    private readonly string[] _mountNames;
-
-    private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"];
-
-    private readonly string[] _grandCompanyNames =
-        ["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"];
 
-    private readonly string[] _supportedCfcOptions =
-    [
-        $"{SeIconChar.Circle.ToIconChar()} Enabled (Default)",
-        $"{SeIconChar.Circle.ToIconChar()} Enabled",
-        $"{SeIconChar.Cross.ToIconChar()} Disabled"
-    ];
-
-    private readonly string[] _unsupportedCfcOptions =
-    [
-        $"{SeIconChar.Cross.ToIconChar()} Disabled (Default)",
-        $"{SeIconChar.Circle.ToIconChar()} Enabled",
-        $"{SeIconChar.Cross.ToIconChar()} Disabled"
-    ];
-
-    private readonly Dictionary<EExpansionVersion, List<DutyInfo>> _contentFinderConditionNames;
-
-    public ConfigWindow(IDalamudPluginInterface pluginInterface,
-        NotificationMasterIpc notificationMasterIpc,
-        Configuration configuration,
-        IDataManager dataManager,
-        CombatController combatController,
-        TerritoryData territoryData,
-        QuestRegistry questRegistry,
-        AutoDutyIpc autoDutyIpc)
+    public ConfigWindow(
+        IDalamudPluginInterface pluginInterface,
+        GeneralConfigComponent generalConfigComponent,
+        DutyConfigComponent dutyConfigComponent,
+        NotificationConfigComponent notificationConfigComponent,
+        DebugConfigComponent debugConfigComponent,
+        Configuration configuration)
         : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
     {
         _pluginInterface = pluginInterface;
-        _notificationMasterIpc = notificationMasterIpc;
+        _generalConfigComponent = generalConfigComponent;
+        _dutyConfigComponent = dutyConfigComponent;
+        _notificationConfigComponent = notificationConfigComponent;
+        _debugConfigComponent = debugConfigComponent;
         _configuration = configuration;
-        _combatController = combatController;
-        _questRegistry = questRegistry;
-        _autoDutyIpc = autoDutyIpc;
-
-        var mounts = dataManager.GetExcelSheet<Mount>()
-            .Where(x => x is { RowId: > 0, Icon: > 0 })
-            .Select(x => (MountId: x.RowId, Name: x.Singular.ToString()))
-            .Where(x => !string.IsNullOrEmpty(x.Name))
-            .OrderBy(x => x.Name)
-            .ToList();
-        _mountIds = DefaultMounts.Select(x => x.Id).Concat(mounts.Select(x => x.MountId)).ToArray();
-        _mountNames = DefaultMounts.Select(x => x.Name).Concat(mounts.Select(x => x.Name)).ToArray();
-
-        _contentFinderConditionNames = dataManager.GetExcelSheet<DawnContent>()
-            .Where(x => x is { RowId: > 0, Unknown16: false })
-            .OrderBy(x => x.Unknown15) // SortKey for the support UI
-            .Select(x => x.Content.ValueNullable)
-            .Where(x => x != null)
-            .Select(x => x!.Value)
-            .Select(x => new
-            {
-                Expansion = (EExpansionVersion)x.TerritoryType.Value.ExVersion.RowId,
-                CfcId = x.RowId,
-                Name = territoryData.GetContentFinderCondition(x.RowId)?.Name ?? "?",
-                TerritoryId = x.TerritoryType.RowId,
-                ContentType = x.ContentType.RowId,
-                Level = x.ClassJobLevelRequired,
-                x.SortKey
-            })
-            .GroupBy(x => x.Expansion)
-            .ToDictionary(x => x.Key,
-                x => x
-                    .Select(y => new DutyInfo(y.CfcId, y.TerritoryId, $"{SeIconChar.LevelEn.ToIconChar()}{FormatLevel(y.Level)} {y.Name}"))
-                    .ToList());
     }
 
     public WindowConfig WindowConfig => _configuration.ConfigWindowConfig;
 
-    private static string FormatLevel(int level)
-    {
-        if (level == 0)
-            return string.Empty;
-
-        return $"{FormatLevel(level / 10)}{(SeIconChar.Number0 + level % 10).ToIconChar()}";
-    }
-
     public override void Draw()
     {
         using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
         if (!tabBar)
             return;
 
-        DrawGeneralTab();
-        DrawDutiesTab();
-        DrawNotificationsTab();
-        DrawAdvancedTab();
-    }
-
-    private void DrawGeneralTab()
-    {
-        using var tab = ImRaii.TabItem("General");
-        if (!tab)
-            return;
-
-        using (ImRaii.Disabled(_combatController.IsRunning))
-        {
-            int selectedCombatModule = (int)_configuration.General.CombatModule;
-            if (ImGui.Combo("Preferred Combat Module", ref selectedCombatModule, _combatModuleNames,
-                    _combatModuleNames.Length))
-            {
-                _configuration.General.CombatModule = (Configuration.ECombatModule)selectedCombatModule;
-                Save();
-            }
-        }
-
-        int selectedMount = Array.FindIndex(_mountIds, x => x == _configuration.General.MountId);
-        if (selectedMount == -1)
-        {
-            selectedMount = 0;
-            _configuration.General.MountId = _mountIds[selectedMount];
-            Save();
-        }
-
-        if (ImGui.Combo("Preferred Mount", ref selectedMount, _mountNames, _mountNames.Length))
-        {
-            _configuration.General.MountId = _mountIds[selectedMount];
-            Save();
-        }
-
-        int grandCompany = (int)_configuration.General.GrandCompany;
-        if (ImGui.Combo("Preferred Grand Company", ref grandCompany, _grandCompanyNames,
-                _grandCompanyNames.Length))
-        {
-            _configuration.General.GrandCompany = (GrandCompany)grandCompany;
-            Save();
-        }
-
-        bool hideInAllInstances = _configuration.General.HideInAllInstances;
-        if (ImGui.Checkbox("Hide quest window in all instanced duties", ref hideInAllInstances))
-        {
-            _configuration.General.HideInAllInstances = hideInAllInstances;
-            Save();
-        }
-
-        bool useEscToCancelQuesting = _configuration.General.UseEscToCancelQuesting;
-        if (ImGui.Checkbox("Use ESC to cancel questing/movement", ref useEscToCancelQuesting))
-        {
-            _configuration.General.UseEscToCancelQuesting = useEscToCancelQuesting;
-            Save();
-        }
-
-        bool showIncompleteSeasonalEvents = _configuration.General.ShowIncompleteSeasonalEvents;
-        if (ImGui.Checkbox("Show details for incomplete seasonal events", ref showIncompleteSeasonalEvents))
-        {
-            _configuration.General.ShowIncompleteSeasonalEvents = showIncompleteSeasonalEvents;
-            Save();
-        }
-
-        bool configureTextAdvance = _configuration.General.ConfigureTextAdvance;
-        if (ImGui.Checkbox("Automatically configure TextAdvance with the recommended settings",
-                ref configureTextAdvance))
-        {
-            _configuration.General.ConfigureTextAdvance = configureTextAdvance;
-            Save();
-        }
-    }
-
-    private void DrawDutiesTab()
-    {
-        using var tab = ImRaii.TabItem("Duties");
-        if (!tab)
-            return;
-
-        bool runInstancedContentWithAutoDuty = _configuration.Duties.RunInstancedContentWithAutoDuty;
-        if (ImGui.Checkbox("Run instanced content with AutoDuty and BossMod", ref runInstancedContentWithAutoDuty))
-        {
-            _configuration.Duties.RunInstancedContentWithAutoDuty = runInstancedContentWithAutoDuty;
-            Save();
-        }
-
-        ImGui.SameLine();
-        ImGuiComponents.HelpMarker(
-            "The combat module used for this is configured by AutoDuty, ignoring whichever selection you've made in Questionable's \"General\" configuration.");
-
-        ImGui.Separator();
-
-        using (ImRaii.Disabled(!runInstancedContentWithAutoDuty))
-        {
-            ImGui.Text(
-                "Questionable includes a default list of duties that work if AutoDuty and BossMod are installed.");
-
-            ImGui.Text("The included list of duties can change with each update, and is based on the following spreadsheet:");
-            if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.GlobeEurope, "Open AutoDuty spreadsheet"))
-                Util.OpenLink(
-                    "https://docs.google.com/spreadsheets/d/151RlpqRcCpiD_VbQn6Duf-u-S71EP7d0mx3j1PDNoNA/edit?pli=1#gid=0");
-
-            ImGui.Separator();
-            ImGui.Text("You can override the dungeon settings for each individual dungeon/trial:");
-
-            using (var child = ImRaii.Child("DutyConfiguration", new Vector2(-1, 400), true))
-            {
-                if (child)
-                {
-                    foreach (EExpansionVersion expansion in Enum.GetValues<EExpansionVersion>())
-                    {
-                        if (ImGui.CollapsingHeader(expansion.ToString()))
-                        {
-                            using var table = ImRaii.Table($"Duties{expansion}", 2, ImGuiTableFlags.SizingFixedFit);
-                            if (table)
-                            {
-                                ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.WidthStretch);
-                                ImGui.TableSetupColumn("Options", ImGuiTableColumnFlags.WidthFixed, 200f);
-
-                                if (_contentFinderConditionNames.TryGetValue(expansion, out var cfcNames))
-                                {
-                                    foreach (var (cfcId, territoryId, name) in cfcNames)
-                                    {
-                                        if (_questRegistry.TryGetDutyByContentFinderConditionId(cfcId,
-                                                out bool autoDutyEnabledByDefault))
-                                        {
-                                            ImGui.TableNextRow();
-
-                                            string[] labels = autoDutyEnabledByDefault
-                                                ? _supportedCfcOptions
-                                                : _unsupportedCfcOptions;
-                                            int value = 0;
-                                            if (_configuration.Duties.WhitelistedDutyCfcIds.Contains(cfcId))
-                                                value = 1;
-                                            if (_configuration.Duties.BlacklistedDutyCfcIds.Contains(cfcId))
-                                                value = 2;
-
-                                            if (ImGui.TableNextColumn())
-                                            {
-                                                ImGui.AlignTextToFramePadding();
-                                                ImGui.TextUnformatted(name);
-                                                if (ImGui.IsItemHovered() && _configuration.Advanced.AdditionalStatusInformation)
-                                                {
-                                                    using var tooltip = ImRaii.Tooltip();
-                                                    if (tooltip)
-                                                    {
-                                                        ImGui.TextUnformatted(name);
-                                                        ImGui.Separator();
-                                                        ImGui.BulletText($"TerritoryId: {territoryId}");
-                                                        ImGui.BulletText($"ContentFinderConditionId: {cfcId}");
-                                                    }
-                                                }
-
-                                                if (runInstancedContentWithAutoDuty && !_autoDutyIpc.HasPath(cfcId))
-                                                    ImGuiComponents.HelpMarker("This duty is not supported by AutoDuty", FontAwesomeIcon.Times, ImGuiColors.DalamudRed);
-                                            }
-
-                                            if (ImGui.TableNextColumn())
-                                            {
-                                                using var _ = ImRaii.PushId($"##Dungeon{cfcId}");
-                                                ImGui.SetNextItemWidth(200);
-                                                if (ImGui.Combo(string.Empty, ref value, labels, labels.Length))
-                                                {
-                                                    _configuration.Duties.WhitelistedDutyCfcIds.Remove(cfcId);
-                                                    _configuration.Duties.BlacklistedDutyCfcIds.Remove(cfcId);
-
-                                                    if (value == 1)
-                                                        _configuration.Duties.WhitelistedDutyCfcIds.Add(cfcId);
-                                                    else if (value == 2)
-                                                        _configuration.Duties.BlacklistedDutyCfcIds.Add(cfcId);
-
-                                                    Save();
-                                                }
-                                            }
-                                        }
-                                    }
-                                }
-                            }
-                        }
-                    }
-                }
-            }
-
-            using (ImRaii.Disabled(_configuration.Duties.WhitelistedDutyCfcIds.Count +
-                       _configuration.Duties.BlacklistedDutyCfcIds.Count == 0))
-            {
-                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Copy, "Export to clipboard"))
-                {
-                    var whitelisted =
-                        _configuration.Duties.WhitelistedDutyCfcIds.Select(x => $"{DutyWhitelistPrefix}{x}");
-                    var blacklisted =
-                        _configuration.Duties.BlacklistedDutyCfcIds.Select(x => $"{DutyBlacklistPrefix}{x}");
-                    string text = DutyClipboardPrefix + Convert.ToBase64String(Encoding.UTF8.GetBytes(
-                        string.Join(DutyClipboardSeparator, whitelisted.Concat(blacklisted))));
-                    ImGui.SetClipboardText(text);
-                }
-            }
-
-            ImGui.SameLine();
-
-            string? clipboardText = GetClipboardText();
-            using (ImRaii.Disabled(clipboardText == null || !clipboardText.StartsWith(DutyClipboardPrefix, StringComparison.InvariantCulture)))
-            {
-                if (ImGuiComponents.IconButtonWithText(FontAwesomeIcon.Paste, "Import from Clipboard"))
-                {
-                    clipboardText = clipboardText!.Substring(DutyClipboardPrefix.Length);
-                    string text = Encoding.UTF8.GetString(Convert.FromBase64String(clipboardText));
-
-                    _configuration.Duties.WhitelistedDutyCfcIds.Clear();
-                    _configuration.Duties.BlacklistedDutyCfcIds.Clear();
-                    foreach (string part in text.Split(DutyClipboardSeparator))
-                    {
-                        if (part.StartsWith(DutyWhitelistPrefix, StringComparison.InvariantCulture) &&
-                            uint.TryParse(part.AsSpan(DutyWhitelistPrefix.Length), CultureInfo.InvariantCulture,
-                                out uint whitelistedCfcId))
-                            _configuration.Duties.WhitelistedDutyCfcIds.Add(whitelistedCfcId);
-
-                        if (part.StartsWith(DutyBlacklistPrefix, StringComparison.InvariantCulture) &&
-                            uint.TryParse(part.AsSpan(DutyBlacklistPrefix.Length), CultureInfo.InvariantCulture,
-                                out uint blacklistedCfcId))
-                            _configuration.Duties.WhitelistedDutyCfcIds.Add(blacklistedCfcId);
-                    }
-                }
-            }
-
-            ImGui.SameLine();
-
-            using (var unused = ImRaii.Disabled(!ImGui.IsKeyDown(ImGuiKey.ModCtrl)))
-            {
-                if (ImGui.Button("Reset to default"))
-                {
-                    _configuration.Duties.WhitelistedDutyCfcIds.Clear();
-                    _configuration.Duties.BlacklistedDutyCfcIds.Clear();
-                    Save();
-                }
-            }
-
-            if (ImGui.IsItemHovered(ImGuiHoveredFlags.AllowWhenDisabled))
-                ImGui.SetTooltip("Hold CTRL to enable this button.");
-        }
-    }
-
-    private void DrawNotificationsTab()
-    {
-        using var tab = ImRaii.TabItem("Notifications");
-        if (!tab)
-            return;
-
-        bool enabled = _configuration.Notifications.Enabled;
-        if (ImGui.Checkbox("Enable notifications when manual interaction is required", ref enabled))
-        {
-            _configuration.Notifications.Enabled = enabled;
-            Save();
-        }
-
-        using (ImRaii.Disabled(!_configuration.Notifications.Enabled))
-        {
-            using (ImRaii.PushIndent())
-            {
-                var xivChatTypes = Enum.GetValues<XivChatType>()
-                    .Where(x => x != XivChatType.StandardEmote)
-                    .ToArray();
-                var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.Notifications.ChatType);
-                string[] chatTypeNames = xivChatTypes
-                    .Select(t => t.GetAttribute<XivChatTypeInfoAttribute>()?.FancyName ?? t.ToString())
-                    .ToArray();
-                if (ImGui.Combo("Chat channel", ref selectedChatType, chatTypeNames,
-                        chatTypeNames.Length))
-                {
-                    _configuration.Notifications.ChatType = xivChatTypes[selectedChatType];
-                    Save();
-                }
-
-                ImGui.Separator();
-                ImGui.Text("NotificationMaster settings");
-                ImGui.SameLine();
-                ImGuiComponents.HelpMarker("Requires the plugin 'NotificationMaster' to be installed.");
-                using (ImRaii.Disabled(!_notificationMasterIpc.Enabled))
-                {
-                    bool showTrayMessage = _configuration.Notifications.ShowTrayMessage;
-                    if (ImGui.Checkbox("Show tray notification", ref showTrayMessage))
-                    {
-                        _configuration.Notifications.ShowTrayMessage = showTrayMessage;
-                        Save();
-                    }
-
-                    bool flashTaskbar = _configuration.Notifications.FlashTaskbar;
-                    if (ImGui.Checkbox("Flash taskbar icon", ref flashTaskbar))
-                    {
-                        _configuration.Notifications.FlashTaskbar = flashTaskbar;
-                        Save();
-                    }
-                }
-            }
-        }
-    }
-
-    private void DrawAdvancedTab()
-    {
-        using var tab = ImRaii.TabItem("Advanced");
-        if (!tab)
-            return;
-
-        ImGui.TextColored(ImGuiColors.DalamudRed,
-            "Enabling any option here may cause unexpected behavior. Use at your own risk.");
-
-        ImGui.Separator();
-
-        bool debugOverlay = _configuration.Advanced.DebugOverlay;
-        if (ImGui.Checkbox("Enable debug overlay", ref debugOverlay))
-        {
-            _configuration.Advanced.DebugOverlay = debugOverlay;
-            Save();
-        }
-
-        bool neverFly = _configuration.Advanced.NeverFly;
-        if (ImGui.Checkbox("Disable flying (even if unlocked for the zone)", ref neverFly))
-        {
-            _configuration.Advanced.NeverFly = neverFly;
-            Save();
-        }
-
-        bool additionalStatusInformation = _configuration.Advanced.AdditionalStatusInformation;
-        if (ImGui.Checkbox("Draw additional status information", ref additionalStatusInformation))
-        {
-            _configuration.Advanced.AdditionalStatusInformation = additionalStatusInformation;
-            Save();
-        }
-
-        ImGui.EndTabItem();
-    }
-
-    private void Save() => _pluginInterface.SavePluginConfig(_configuration);
-
-    public void SaveWindowConfig() => Save();
-
-    /// <summary>
-    /// The default implementation for <see cref="ImGui.GetClipboardText"/> throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text.
-    /// </summary>
-    private unsafe string? GetClipboardText()
-    {
-        byte* ptr = ImGuiNative.igGetClipboardText();
-        if (ptr == null)
-            return null;
-
-        int byteCount = 0;
-        while (ptr[byteCount] != 0)
-            ++byteCount;
-        return Encoding.UTF8.GetString(ptr, byteCount);
+        _generalConfigComponent.DrawTab();
+        _dutyConfigComponent.DrawTab();
+        _notificationConfigComponent.DrawTab();
+        _debugConfigComponent.DrawTab();
     }
 
-    private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
+    public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
 }