diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index ccd694af..0f776eb6 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -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(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); } private static void AddQuestValidators(ServiceCollection serviceCollection) diff --git a/Questionable/Windows/ConfigComponents/ConfigComponent.cs b/Questionable/Windows/ConfigComponents/ConfigComponent.cs new file mode 100644 index 00000000..0a5be627 --- /dev/null +++ b/Questionable/Windows/ConfigComponents/ConfigComponent.cs @@ -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()}"; + } + + /// + /// The default implementation for throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text. + /// + 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 index 00000000..7d89efd0 --- /dev/null +++ b/Questionable/Windows/ConfigComponents/DebugConfigComponent.cs @@ -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 index 00000000..ffb6538a --- /dev/null +++ b/Questionable/Windows/ConfigComponents/DutyConfigComponent.cs @@ -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> _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() + .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()) + { + 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 index 00000000..e58ce989 --- /dev/null +++ b/Questionable/Windows/ConfigComponents/GeneralConfigComponent.cs @@ -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() + .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 index 00000000..d0a4ba0d --- /dev/null +++ b/Questionable/Windows/ConfigComponents/NotificationConfigComponent.cs @@ -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() + .Where(x => x != XivChatType.StandardEmote) + .ToArray(); + var selectedChatType = Array.IndexOf(xivChatTypes, Configuration.Notifications.ChatType); + string[] chatTypeNames = xivChatTypes + .Select(t => t.GetAttribute()?.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(); + } + } + } + } + } +} diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index 131f3726..e2ac6c31 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -1,485 +1,50 @@ -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> _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() - .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() - .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(); + _generalConfigComponent.DrawTab(); + _dutyConfigComponent.DrawTab(); + _notificationConfigComponent.DrawTab(); + _debugConfigComponent.DrawTab(); } - 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()) - { - 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() - .Where(x => x != XivChatType.StandardEmote) - .ToArray(); - var selectedChatType = Array.IndexOf(xivChatTypes, _configuration.Notifications.ChatType); - string[] chatTypeNames = xivChatTypes - .Select(t => t.GetAttribute()?.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(); - - /// - /// The default implementation for throws an NullReferenceException if the clipboard is empty, maybe also if it doesn't contain text. - /// - 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); - } - - private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name); + public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration); }