Restructure config window

This commit is contained in:
Liza 2025-02-20 21:16:48 +01:00
parent 097c67ed5d
commit 3820647827
Signed by: liza
GPG Key ID: 2C41B84815CF6445
7 changed files with 584 additions and 457 deletions

View File

@ -26,6 +26,7 @@ using Questionable.Functions;
using Questionable.Validation; using Questionable.Validation;
using Questionable.Validation.Validators; using Questionable.Validation.Validators;
using Questionable.Windows; using Questionable.Windows;
using Questionable.Windows.ConfigComponents;
using Questionable.Windows.JournalComponents; using Questionable.Windows.JournalComponents;
using Questionable.Windows.QuestComponents; using Questionable.Windows.QuestComponents;
using Action = Questionable.Controller.Steps.Interactions.Action; using Action = Questionable.Controller.Steps.Interactions.Action;
@ -298,6 +299,11 @@ public sealed class QuestionablePlugin : IDalamudPlugin
serviceCollection.AddSingleton<QuestValidationWindow>(); serviceCollection.AddSingleton<QuestValidationWindow>();
serviceCollection.AddSingleton<JournalProgressWindow>(); serviceCollection.AddSingleton<JournalProgressWindow>();
serviceCollection.AddSingleton<PriorityWindow>(); serviceCollection.AddSingleton<PriorityWindow>();
serviceCollection.AddSingleton<GeneralConfigComponent>();
serviceCollection.AddSingleton<DutyConfigComponent>();
serviceCollection.AddSingleton<NotificationConfigComponent>();
serviceCollection.AddSingleton<DebugConfigComponent>();
} }
private static void AddQuestValidators(ServiceCollection serviceCollection) private static void AddQuestValidators(ServiceCollection serviceCollection)

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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();
}
}
}

View File

@ -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();
}
}
}
}
}
}

View File

@ -1,485 +1,50 @@
using System; using Dalamud.Interface.Utility.Raii;
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;
using Dalamud.Plugin.Services;
using Dalamud.Utility;
using ImGuiNET; using ImGuiNET;
using LLib.ImGui; using LLib.ImGui;
using Lumina.Excel.Sheets; using Questionable.Windows.ConfigComponents;
using Questionable.Controller;
using Questionable.Data;
using Questionable.External;
using Questionable.Model;
using GrandCompany = FFXIVClientStructs.FFXIV.Client.UI.Agent.GrandCompany;
namespace Questionable.Windows; namespace Questionable.Windows;
internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig 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 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 Configuration _configuration;
private readonly CombatController _combatController;
private readonly QuestRegistry _questRegistry;
private readonly AutoDutyIpc _autoDutyIpc;
private readonly uint[] _mountIds; public ConfigWindow(
private readonly string[] _mountNames; IDalamudPluginInterface pluginInterface,
GeneralConfigComponent generalConfigComponent,
private readonly string[] _combatModuleNames = ["None", "Boss Mod (VBM)", "Wrath Combo", "Rotation Solver Reborn"]; DutyConfigComponent dutyConfigComponent,
NotificationConfigComponent notificationConfigComponent,
private readonly string[] _grandCompanyNames = DebugConfigComponent debugConfigComponent,
["None (manually pick quest)", "Maelstrom", "Twin Adder", "Immortal Flames"]; Configuration configuration)
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)
: base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize) : base("Config - Questionable###QuestionableConfig", ImGuiWindowFlags.AlwaysAutoResize)
{ {
_pluginInterface = pluginInterface; _pluginInterface = pluginInterface;
_notificationMasterIpc = notificationMasterIpc; _generalConfigComponent = generalConfigComponent;
_dutyConfigComponent = dutyConfigComponent;
_notificationConfigComponent = notificationConfigComponent;
_debugConfigComponent = debugConfigComponent;
_configuration = configuration; _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; 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() public override void Draw()
{ {
using var tabBar = ImRaii.TabBar("QuestionableConfigTabs"); using var tabBar = ImRaii.TabBar("QuestionableConfigTabs");
if (!tabBar) if (!tabBar)
return; return;
DrawGeneralTab(); _generalConfigComponent.DrawTab();
DrawDutiesTab(); _dutyConfigComponent.DrawTab();
DrawNotificationsTab(); _notificationConfigComponent.DrawTab();
DrawAdvancedTab(); _debugConfigComponent.DrawTab();
} }
private void DrawGeneralTab() public void SaveWindowConfig() => _pluginInterface.SavePluginConfig(_configuration);
{
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);
}
private sealed record DutyInfo(uint CfcId, uint TerritoryId, string Name);
} }