From: Liza Carvelli Date: Wed, 13 Aug 2025 10:08:47 +0000 (+0200) Subject: Add stop conditions X-Git-Tag: v6.1~3 X-Git-Url: https://git.jacobcasper.com/?a=commitdiff_plain;h=6aa189419ee92fab2170bb345f66532cab7a992f;p=Questionable.git Add stop conditions --- diff --git a/Questionable/Configuration.cs b/Questionable/Configuration.cs index 1aae915c..f3230589 100644 --- a/Questionable/Configuration.cs +++ b/Questionable/Configuration.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Dalamud.Configuration; using Dalamud.Game.Text; using FFXIVClientStructs.FFXIV.Client.UI.Agent; using LLib.GameData; using LLib.ImGui; +using Newtonsoft.Json; +using Questionable.Model.Questing; namespace Questionable; @@ -15,6 +18,7 @@ internal sealed class Configuration : IPluginConfiguration public int Version { get; set; } = 1; public int PluginSetupCompleteVersion { get; set; } public GeneralConfiguration General { get; } = new(); + public StopConfiguration Stop { get; } = new(); public DutyConfiguration Duties { get; } = new(); public SinglePlayerDutyConfiguration SinglePlayerDuties { get; } = new(); public NotificationConfiguration Notifications { get; } = new(); @@ -39,6 +43,14 @@ internal sealed class Configuration : IPluginConfiguration public bool ConfigureTextAdvance { get; set; } = true; } + internal sealed class StopConfiguration + { + public bool Enabled { get; set; } + + [JsonProperty(ItemConverterType = typeof(ElementIdNConverter))] + public List QuestsToStopAfter { get; set; } = []; + } + internal sealed class DutyConfiguration { public bool RunInstancedContentWithAutoDuty { get; set; } @@ -85,4 +97,19 @@ internal sealed class Configuration : IPluginConfiguration WrathCombo, RotationSolverReborn, } + + public sealed class ElementIdNConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, ElementId? value, JsonSerializer serializer) + { + writer.WriteValue(value?.ToString()); + } + + public override ElementId? ReadJson(JsonReader reader, Type objectType, ElementId? existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + string? value = reader.Value?.ToString(); + return value != null ? ElementId.FromString(value) : null; + } + } } diff --git a/Questionable/Controller/QuestController.cs b/Questionable/Controller/QuestController.cs index 6387f3b6..13cfba17 100644 --- a/Questionable/Controller/QuestController.cs +++ b/Questionable/Controller/QuestController.cs @@ -365,7 +365,18 @@ internal sealed class QuestController : MiniTaskController } else if (_startedQuest == null || _startedQuest.Quest.Id != currentQuestId) { - if (_questRegistry.TryGetQuest(currentQuestId, out var quest)) + if (_configuration.Stop.Enabled && + _startedQuest != null && + _configuration.Stop.QuestsToStopAfter.Contains(_startedQuest.Quest.Id) && + _questFunctions.IsQuestComplete(_startedQuest.Quest.Id)) + { + var questId = _startedQuest.Quest.Id; + _logger.LogInformation("Reached stopping point (quest: {QuestId})", questId); + _chatGui.Print($"Completed quest '{_startedQuest.Quest.Info.Name}', which is configured as a stopping point.", CommandHandler.MessageTag, CommandHandler.TagColor); + _startedQuest = null; + Stop($"Stopping point [{questId}] reached"); + } + else if (_questRegistry.TryGetQuest(currentQuestId, out var quest)) { _logger.LogInformation("New quest: {QuestName}", quest.Info.Name); _startedQuest = new QuestProgress(quest, currentSequence); diff --git a/Questionable/QuestionablePlugin.cs b/Questionable/QuestionablePlugin.cs index 3b96efd1..d130b513 100644 --- a/Questionable/QuestionablePlugin.cs +++ b/Questionable/QuestionablePlugin.cs @@ -30,6 +30,7 @@ using Questionable.Windows; using Questionable.Windows.ConfigComponents; using Questionable.Windows.JournalComponents; using Questionable.Windows.QuestComponents; +using Questionable.Windows.Utils; using Action = Questionable.Controller.Steps.Interactions.Action; namespace Questionable; @@ -279,6 +280,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin private static void AddWindows(ServiceCollection serviceCollection) { serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); @@ -307,6 +309,7 @@ public sealed class QuestionablePlugin : IDalamudPlugin serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); } diff --git a/Questionable/Windows/ConfigComponents/StopConditionComponent.cs b/Questionable/Windows/ConfigComponents/StopConditionComponent.cs new file mode 100644 index 00000000..9b07bd3e --- /dev/null +++ b/Questionable/Windows/ConfigComponents/StopConditionComponent.cs @@ -0,0 +1,119 @@ +using System.Collections.Generic; +using System.Linq; +using Dalamud.Bindings.ImGui; +using Dalamud.Interface; +using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; +using Dalamud.Plugin; +using Questionable.Controller; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; +using Questionable.Windows.QuestComponents; +using Questionable.Windows.Utils; + +namespace Questionable.Windows.ConfigComponents; + +internal sealed class StopConditionComponent : ConfigComponent +{ + private readonly IDalamudPluginInterface _pluginInterface; + private readonly QuestSelector _questSelector; + private readonly QuestRegistry _questRegistry; + private readonly QuestTooltipComponent _questTooltipComponent; + private readonly UiUtils _uiUtils; + + public StopConditionComponent( + IDalamudPluginInterface pluginInterface, + QuestSelector questSelector, + QuestFunctions questFunctions, + QuestRegistry questRegistry, + QuestTooltipComponent questTooltipComponent, + UiUtils uiUtils, + Configuration configuration) + : base(pluginInterface, configuration) + { + _pluginInterface = pluginInterface; + _questSelector = questSelector; + _questRegistry = questRegistry; + _questTooltipComponent = questTooltipComponent; + _uiUtils = uiUtils; + + _questSelector.SuggestionPredicate = quest => configuration.Stop.QuestsToStopAfter.All(x => x != quest.Id); + _questSelector.DefaultPredicate = quest => quest.Info.IsMainScenarioQuest && questFunctions.IsQuestAccepted(quest.Id); + _questSelector.QuestSelected = quest => + { + configuration.Stop.QuestsToStopAfter.Add(quest.Id); + Save(); + }; + } + + public override void DrawTab() + { + using var tab = ImRaii.TabItem("Stop###StopConditionns"); + if (!tab) + return; + + bool enabled = Configuration.Stop.Enabled; + if (ImGui.Checkbox("Stop Questionable when completing any of the quests selected below", ref enabled)) + { + Configuration.Stop.Enabled = enabled; + Save(); + } + + ImGui.Separator(); + + using (ImRaii.Disabled(!enabled)) + { + ImGui.Text("Quests to stop after:"); + + _questSelector.DrawSelection(); + + List questsToStopAfter = Configuration.Stop.QuestsToStopAfter; + Quest? itemToRemove = null; + for (int i = 0; i < questsToStopAfter.Count; i++) + { + ElementId questId = questsToStopAfter[i]; + + if (!_questRegistry.TryGetQuest(questId, out Quest? quest)) + continue; + + using (ImRaii.PushId($"Quest{questId}")) + { + var style = _uiUtils.GetQuestStyle(questId); + bool hovered; + using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(style.Color, style.Icon.ToIconString()); + hovered = ImGui.IsItemHovered(); + } + + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(quest.Info.Name); + hovered |= ImGui.IsItemHovered(); + + if (hovered) + _questTooltipComponent.Draw(quest.Info); + + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.SameLine(ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().WindowPadding.X - + ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - + ImGui.GetStyle().FramePadding.X * 2); + } + + if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) + itemToRemove = quest; + } + } + + if (itemToRemove != null) + { + Configuration.Stop.QuestsToStopAfter.Remove(itemToRemove.Id); + Save(); + } + } + } +} diff --git a/Questionable/Windows/ConfigWindow.cs b/Questionable/Windows/ConfigWindow.cs index 994eae28..96aa1308 100644 --- a/Questionable/Windows/ConfigWindow.cs +++ b/Questionable/Windows/ConfigWindow.cs @@ -13,6 +13,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig private readonly PluginConfigComponent _pluginConfigComponent; private readonly DutyConfigComponent _dutyConfigComponent; private readonly SinglePlayerDutyConfigComponent _singlePlayerDutyConfigComponent; + private readonly StopConditionComponent _stopConditionComponent; private readonly NotificationConfigComponent _notificationConfigComponent; private readonly DebugConfigComponent _debugConfigComponent; private readonly Configuration _configuration; @@ -23,6 +24,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig PluginConfigComponent pluginConfigComponent, DutyConfigComponent dutyConfigComponent, SinglePlayerDutyConfigComponent singlePlayerDutyConfigComponent, + StopConditionComponent stopConditionComponent, NotificationConfigComponent notificationConfigComponent, DebugConfigComponent debugConfigComponent, Configuration configuration) @@ -33,6 +35,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _pluginConfigComponent = pluginConfigComponent; _dutyConfigComponent = dutyConfigComponent; _singlePlayerDutyConfigComponent = singlePlayerDutyConfigComponent; + _stopConditionComponent = stopConditionComponent; _notificationConfigComponent = notificationConfigComponent; _debugConfigComponent = debugConfigComponent; _configuration = configuration; @@ -50,6 +53,7 @@ internal sealed class ConfigWindow : LWindow, IPersistableWindowConfig _pluginConfigComponent.DrawTab(); _dutyConfigComponent.DrawTab(); _singlePlayerDutyConfigComponent.DrawTab(); + _stopConditionComponent.DrawTab(); _notificationConfigComponent.DrawTab(); _debugConfigComponent.DrawTab(); } diff --git a/Questionable/Windows/PriorityWindow.cs b/Questionable/Windows/PriorityWindow.cs index cd933d79..6b14a677 100644 --- a/Questionable/Windows/PriorityWindow.cs +++ b/Questionable/Windows/PriorityWindow.cs @@ -7,6 +7,7 @@ using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Components; +using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Dalamud.Plugin.Services; using LLib.ImGui; @@ -15,6 +16,7 @@ using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Windows.QuestComponents; +using Questionable.Windows.Utils; namespace Questionable.Windows; @@ -24,29 +26,35 @@ internal sealed class PriorityWindow : LWindow private const char ClipboardSeparator = ';'; private readonly QuestController _questController; - private readonly QuestRegistry _questRegistry; private readonly QuestFunctions _questFunctions; + private readonly QuestSelector _questSelector; private readonly QuestTooltipComponent _questTooltipComponent; private readonly UiUtils _uiUtils; private readonly IChatGui _chatGui; private readonly IDalamudPluginInterface _pluginInterface; - private string _searchString = string.Empty; private ElementId? _draggedItem; - public PriorityWindow(QuestController questController, QuestRegistry questRegistry, QuestFunctions questFunctions, + public PriorityWindow(QuestController questController, QuestFunctions questFunctions, QuestSelector questSelector, QuestTooltipComponent questTooltipComponent, UiUtils uiUtils, IChatGui chatGui, IDalamudPluginInterface pluginInterface) : base("Quest Priority###QuestionableQuestPriority") { _questController = questController; - _questRegistry = questRegistry; _questFunctions = questFunctions; + _questSelector = questSelector; _questTooltipComponent = questTooltipComponent; _uiUtils = uiUtils; _chatGui = chatGui; _pluginInterface = pluginInterface; + _questSelector.SuggestionPredicate = quest => + !quest.Info.IsMainScenarioQuest && + !questFunctions.IsQuestUnobtainable(quest.Id) && + questController.ManualPriorityQuests.All(x => x.Id != quest.Id); + _questSelector.DefaultPredicate = quest => questFunctions.IsQuestAccepted(quest.Id); + _questSelector.QuestSelected = quest => _questController.ManualPriorityQuests.Add(quest); + Size = new Vector2(400, 400); SizeCondition = ImGuiCond.Once; SizeConstraints = new WindowSizeConstraints @@ -59,7 +67,7 @@ internal sealed class PriorityWindow : LWindow public override void DrawContent() { ImGui.Text("Quests to do first:"); - DrawQuestFilter(); + _questSelector.DrawSelection(); DrawQuestList(); List clipboardItems = ParseClipboardItems(); @@ -92,60 +100,6 @@ internal sealed class PriorityWindow : LWindow "If you don't have any active MSQ quest, it will always try to pick up the next quest in the MSQ first."); } - private void DrawQuestFilter() - { - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - if (ImGui.BeginCombo($"##QuestSelection", "Add Quest...", ImGuiComboFlags.HeightLarge)) - { - ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); - bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256, - ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue); - - IEnumerable foundQuests; - if (!string.IsNullOrEmpty(_searchString)) - { - bool DefaultPredicate(Quest x) => x.Info.Name.Contains(_searchString, StringComparison.CurrentCultureIgnoreCase); - - Func searchPredicate; - if (ElementId.TryFromString(_searchString, out ElementId? elementId)) - searchPredicate = x => DefaultPredicate(x) || x.Id == elementId; - else - searchPredicate = DefaultPredicate; - - foundQuests = _questRegistry.AllQuests - .Where(x => x.Id is not SatisfactionSupplyNpcId and not AlliedSocietyDailyId) - .Where(searchPredicate) - .Where(x => !_questFunctions.IsQuestUnobtainable(x.Id)); - } - else - { - foundQuests = _questRegistry.AllQuests.Where(x => _questFunctions.IsQuestAccepted(x.Id)); - } - - foreach (var quest in foundQuests) - { - if (quest.Info.IsMainScenarioQuest || _questController.ManualPriorityQuests.Any(x => x.Id == quest.Id)) - continue; - - bool addThis = ImGui.Selectable(quest.Info.Name); - if (addThis || addFirst) - { - _questController.ManualPriorityQuests.Add(quest); - - if (addFirst) - { - ImGui.CloseCurrentPopup(); - addFirst = false; - } - } - } - - ImGui.EndCombo(); - } - - ImGui.Spacing(); - } - private void DrawQuestList() { List priorityQuests = _questController.ManualPriorityQuests; @@ -161,63 +115,64 @@ internal sealed class PriorityWindow : LWindow Vector2 topLeft = ImGui.GetCursorScreenPos() + new Vector2(0, -ImGui.GetStyle().ItemSpacing.Y / 2); var quest = priorityQuests[i]; - ImGui.PushID($"Quest{quest.Id}"); - - var style = _uiUtils.GetQuestStyle(quest.Id); - bool hovered; - using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + using (ImRaii.PushId($"Quest{quest.Id}")) { - ImGui.AlignTextToFramePadding(); - ImGui.TextColored(style.Color, style.Icon.ToIconString()); - hovered = ImGui.IsItemHovered(); - } + var style = _uiUtils.GetQuestStyle(quest.Id); + bool hovered; + using (var _ = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) + { + ImGui.AlignTextToFramePadding(); + ImGui.TextColored(style.Color, style.Icon.ToIconString()); + hovered = ImGui.IsItemHovered(); + } - ImGui.SameLine(); - ImGui.AlignTextToFramePadding(); - ImGui.Text(quest.Info.Name); - hovered |= ImGui.IsItemHovered(); + ImGui.SameLine(); + ImGui.AlignTextToFramePadding(); + ImGui.Text(quest.Info.Name); + hovered |= ImGui.IsItemHovered(); - if (hovered) - _questTooltipComponent.Draw(quest.Info); + if (hovered) + _questTooltipComponent.Draw(quest.Info); - if (priorityQuests.Count > 1) - { - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SameLine(ImGui.GetContentRegionAvail().X + - ImGui.GetStyle().WindowPadding.X - - ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - - ImGui.GetStyle().FramePadding.X * 4 - - ImGui.GetStyle().ItemSpacing.X); - ImGui.PopFont(); - - if (_draggedItem == quest.Id) + if (priorityQuests.Count > 1) { - ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, - ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive))); - } - else - ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown); + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.SameLine(ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().WindowPadding.X - + ImGui.CalcTextSize(FontAwesomeIcon.ArrowsUpDown.ToIconString()).X - + ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - + ImGui.GetStyle().FramePadding.X * 4 - + ImGui.GetStyle().ItemSpacing.X); + } - if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) - _draggedItem = quest.Id; + if (_draggedItem == quest.Id) + { + ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown, + ImGui.ColorConvertU32ToFloat4(ImGui.GetColorU32(ImGuiCol.ButtonActive))); + } + else + ImGuiComponents.IconButton("##Move", FontAwesomeIcon.ArrowsUpDown); - ImGui.SameLine(); - } - else - { - ImGui.PushFont(UiBuilder.IconFont); - ImGui.SameLine(ImGui.GetContentRegionAvail().X + - ImGui.GetStyle().WindowPadding.X - - ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - - ImGui.GetStyle().FramePadding.X * 2); - ImGui.PopFont(); - } + if (_draggedItem == null && ImGui.IsItemActive() && ImGui.IsMouseDragging(ImGuiMouseButton.Left)) + _draggedItem = quest.Id; - if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) - itemToRemove = quest; + ImGui.SameLine(); + } + else + { + using (ImRaii.PushFont(UiBuilder.IconFont)) + { + ImGui.SameLine(ImGui.GetContentRegionAvail().X + + ImGui.GetStyle().WindowPadding.X - + ImGui.CalcTextSize(FontAwesomeIcon.Times.ToIconString()).X - + ImGui.GetStyle().FramePadding.X * 2); + } + } - ImGui.PopID(); + if (ImGuiComponents.IconButton($"##Remove{i}", FontAwesomeIcon.Times)) + itemToRemove = quest; + } Vector2 bottomRight = new Vector2(topLeft.X + width, ImGui.GetCursorScreenPos().Y - ImGui.GetStyle().ItemSpacing.Y + 2); diff --git a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs index 586d260c..d9b156ad 100644 --- a/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs +++ b/Questionable/Windows/QuestComponents/ActiveQuestComponent.cs @@ -33,6 +33,7 @@ internal sealed partial class ActiveQuestComponent private readonly Configuration _configuration; private readonly QuestRegistry _questRegistry; private readonly PriorityWindow _priorityWindow; + private readonly UiUtils _uiUtils; private readonly IChatGui _chatGui; private readonly ILogger _logger; @@ -46,6 +47,7 @@ internal sealed partial class ActiveQuestComponent Configuration configuration, QuestRegistry questRegistry, PriorityWindow priorityWindow, + UiUtils uiUtils, IChatGui chatGui, ILogger logger) { @@ -58,6 +60,7 @@ internal sealed partial class ActiveQuestComponent _configuration = configuration; _questRegistry = questRegistry; _priorityWindow = priorityWindow; + _uiUtils = uiUtils; _chatGui = chatGui; _logger = logger; } @@ -184,6 +187,29 @@ internal sealed partial class ActiveQuestComponent ImGui.TextColored(ImGuiColors.DalamudRed, "Disabled"); } + if (_configuration.Stop.Enabled && + _configuration.Stop.QuestsToStopAfter.Any(x => !_questFunctions.IsQuestComplete(x) && !_questFunctions.IsQuestUnobtainable(x))) + { + ImGui.SameLine(); + ImGui.TextColored(ImGuiColors.ParsedPurple, SeIconChar.Clock.ToIconString()); + if (ImGui.IsItemHovered()) + { + using var tooltip = ImRaii.Tooltip(); + if (tooltip) + { + ImGui.Text("Questionable will stop after completing any of the following quests:"); + foreach (var questId in _configuration.Stop.QuestsToStopAfter) + { + if (_questRegistry.TryGetQuest(questId, out var quest)) + { + (Vector4 color, FontAwesomeIcon icon, _) = _uiUtils.GetQuestStyle(questId); + _uiUtils.ChecklistItem($"{quest.Info.Name} ({questId})", color, icon); + } + } + } + } + } + if (_configuration.Advanced.AdditionalStatusInformation && _questController.IsInterruptible()) { ImGui.SameLine(); diff --git a/Questionable/Windows/Utils/QuestSelector.cs b/Questionable/Windows/Utils/QuestSelector.cs new file mode 100644 index 00000000..1220d8cf --- /dev/null +++ b/Questionable/Windows/Utils/QuestSelector.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Dalamud.Bindings.ImGui; +using Questionable.Controller; +using Questionable.Functions; +using Questionable.Model; +using Questionable.Model.Questing; + +namespace Questionable.Windows.Utils; + +internal sealed class QuestSelector(QuestRegistry questRegistry, QuestFunctions questFunctions) +{ + private string _searchString = string.Empty; + + public Predicate? SuggestionPredicate { private get; set; } + public Predicate? DefaultPredicate { private get; set; } + public Action? QuestSelected { private get; set; } + + public void DrawSelection() + { + if (QuestSelected == null) + throw new InvalidOperationException("QuestSelected action must be set before drawing the quest selector."); + + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + if (ImGui.BeginCombo("##QuestSelection", "Add Quest...", ImGuiComboFlags.HeightLarge)) + { + ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); + bool addFirst = ImGui.InputTextWithHint("", "Filter...", ref _searchString, 256, + ImGuiInputTextFlags.AutoSelectAll | ImGuiInputTextFlags.EnterReturnsTrue); + + IEnumerable foundQuests; + if (!string.IsNullOrEmpty(_searchString)) + { + bool DefaultPredicate(Quest x) => + x.Info.Name.Contains(_searchString, StringComparison.CurrentCultureIgnoreCase); + + Func searchPredicate; + if (ElementId.TryFromString(_searchString, out ElementId? elementId)) + searchPredicate = x => DefaultPredicate(x) || x.Id == elementId; + else + searchPredicate = DefaultPredicate; + + foundQuests = questRegistry.AllQuests + .Where(x => x.Id is not SatisfactionSupplyNpcId and not AlliedSocietyDailyId) + .Where(searchPredicate); + } + else + { + foundQuests = questRegistry.AllQuests.Where(x => DefaultPredicate?.Invoke(x) ?? true); + } + + foreach (var quest in foundQuests) + { + if (SuggestionPredicate != null && !SuggestionPredicate.Invoke(quest)) + continue; + + bool addThis = ImGui.Selectable(quest.Info.Name); + if (addThis || addFirst) + { + QuestSelected(quest); + + if (addFirst) + { + ImGui.CloseCurrentPopup(); + addFirst = false; + } + } + } + + ImGui.EndCombo(); + } + + ImGui.Spacing(); + } +}