using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Colors; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Questionable.Controller; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Validation; using Questionable.Windows.QuestComponents; namespace Questionable.Windows.JournalComponents; internal sealed class QuestJournalComponent { private sealed record FilteredSection(JournalData.Section Section, List Categories); private sealed record FilteredCategory(JournalData.Category Category, List Genres); private sealed record FilteredGenre(JournalData.Genre Genre, List Quests); private sealed record JournalCounts(int Available, int Total, int Obtainable, int Completed) { public JournalCounts() : this(0, 0, 0, 0) { } } internal sealed class FilterConfiguration { public string SearchText = string.Empty; public bool AvailableOnly; public bool HideNoPaths; public bool AdvancedFiltersActive { get { if (!AvailableOnly) { return HideNoPaths; } return true; } } public FilterConfiguration WithoutName() { return new FilterConfiguration { AvailableOnly = AvailableOnly, HideNoPaths = HideNoPaths }; } } private readonly Dictionary _genreCounts = new Dictionary(); private readonly Dictionary _categoryCounts = new Dictionary(); private readonly Dictionary _sectionCounts = new Dictionary(); private readonly JournalData _journalData; private readonly QuestRegistry _questRegistry; private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; private readonly QuestTooltipComponent _questTooltipComponent; private readonly IDalamudPluginInterface _pluginInterface; private readonly QuestJournalUtils _questJournalUtils; private readonly QuestValidator _questValidator; private List _filteredSections = new List(); internal FilterConfiguration Filter { get; } = new FilterConfiguration(); public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, QuestJournalUtils questJournalUtils, QuestValidator questValidator) { _journalData = journalData; _questRegistry = questRegistry; _questFunctions = questFunctions; _uiUtils = uiUtils; _questTooltipComponent = questTooltipComponent; _pluginInterface = pluginInterface; _questJournalUtils = questJournalUtils; _questValidator = questValidator; } public void DrawQuests() { using ImRaii.IEndObject endObject = ImRaii.TabItem("Quests"); if (!endObject) { return; } if (ImGui.CollapsingHeader("Explanation", ImGuiTreeNodeFlags.DefaultOpen)) { ImGui.Text("The list below contains all quests that appear in your journal."); ImGui.BulletText("'Supported' lists quests that Questionable can do for you"); ImGui.BulletText("'Completed' lists quests your current character has completed."); ImGui.BulletText("Not all quests can be completed even if they're listed as available, e.g. starting city quest chains."); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); } QuestJournalUtils.ShowFilterContextMenu(this); ImGui.SameLine(); ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X); if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref Filter.SearchText, 256)) { UpdateFilter(); } if (_filteredSections.Count > 0) { using (ImRaii.IEndObject endObject2 = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings)) { if (!endObject2) { return; } ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide); ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120f * ImGui.GetIO().FontGlobalScale); ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120f * ImGui.GetIO().FontGlobalScale); ImGui.TableHeadersRow(); foreach (FilteredSection filteredSection in _filteredSections) { DrawSection(filteredSection); } return; } } ImGui.Text("No quest or category matches your search."); } private void DrawSection(FilteredSection filter) { var (count, num5, total, count2) = _sectionCounts.GetValueOrDefault(filter.Section, new JournalCounts()); if (num5 == 0) { return; } ImGui.TableNextRow(); ImGui.TableNextColumn(); bool num6 = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); DrawCount(count, num5); ImGui.TableNextColumn(); DrawCount(count2, total); if (!num6) { return; } foreach (FilteredCategory category in filter.Categories) { DrawCategory(category); } ImGui.TreePop(); } private void DrawCategory(FilteredCategory filter) { var (count, num5, total, count2) = _categoryCounts.GetValueOrDefault(filter.Category, new JournalCounts()); if (num5 == 0) { return; } ImGui.TableNextRow(); ImGui.TableNextColumn(); bool num6 = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); DrawCount(count, num5); ImGui.TableNextColumn(); DrawCount(count2, total); if (!num6) { return; } foreach (FilteredGenre genre in filter.Genres) { DrawGenre(genre); } ImGui.TreePop(); } private void DrawGenre(FilteredGenre filter) { var (count, num5, total, count2) = _genreCounts.GetValueOrDefault(filter.Genre, new JournalCounts()); if (num5 == 0) { return; } ImGui.TableNextRow(); ImGui.TableNextColumn(); bool num6 = ImGui.TreeNodeEx(filter.Genre.Name, ImGuiTreeNodeFlags.SpanFullWidth); ImGui.TableNextColumn(); DrawCount(count, num5); ImGui.TableNextColumn(); DrawCount(count2, total); if (!num6) { return; } foreach (IQuestInfo quest in filter.Quests) { DrawQuest(quest); } ImGui.TreePop(); } private void DrawQuest(IQuestInfo questInfo) { _questRegistry.TryGetQuest(questInfo.QuestId, out Quest quest); ImGui.TableNextRow(); ImGui.TableNextColumn(); ImU8String id = new ImU8String(3, 2); id.AppendFormatted(questInfo.Name); id.AppendLiteral(" ("); id.AppendFormatted(questInfo.QuestId); id.AppendLiteral(")"); ImGui.TreeNodeEx(id, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.SpanFullWidth); if (ImGui.IsItemHovered()) { _questTooltipComponent.Draw(questInfo); } _questJournalUtils.ShowContextMenu(questInfo, quest, "QuestJournalComponent"); ImGui.TableNextColumn(); float num; using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { num = ImGui.GetColumnWidth() / 2f - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X; } ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); if (_questFunctions.IsQuestRemoved(questInfo.QuestId)) { _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus); } else { if (quest != null) { QuestRoot root = quest.Root; if (root != null && !root.Disabled) { List issues = _questValidator.GetIssues(quest.Id); if (issues.Any((ValidationIssue x) => x.Severity == EIssueSeverity.Error)) { _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudRed, FontAwesomeIcon.ExclamationTriangle); } else if (issues.Count > 0) { _uiUtils.ChecklistItem(string.Empty, ImGuiColors.ParsedBlue, FontAwesomeIcon.InfoCircle); } else { _uiUtils.ChecklistItem(string.Empty, complete: true); } goto IL_0215; } } _uiUtils.ChecklistItem(string.Empty, complete: false); } goto IL_0215; IL_0215: ImGui.TableNextColumn(); var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId); _uiUtils.ChecklistItem(text, color, icon); } private static void DrawCount(int count, int total) { string text = 9999.ToString(CultureInfo.CurrentCulture); ImGui.PushFont(UiBuilder.MonoFont); if (total == 0) { Vector4 col = ImGuiColors.DalamudGrey; ImU8String text2 = new ImU8String(3, 2); text2.AppendFormatted("-".PadLeft(text.Length)); text2.AppendLiteral(" / "); text2.AppendFormatted("-".PadLeft(text.Length)); ImGui.TextColored(in col, text2); } else { string text3 = count.ToString(CultureInfo.CurrentCulture).PadLeft(text.Length) + " / " + total.ToString(CultureInfo.CurrentCulture).PadLeft(text.Length); if (count == total) { ImGui.TextColored(ImGuiColors.ParsedGreen, text3); } else { ImGui.TextUnformatted(text3); } } ImGui.PopFont(); } public void UpdateFilter() { _filteredSections = (from x in _journalData.Sections select FilterSection(x, Filter) into x where x.Categories.Count > 0 select x).ToList(); RefreshCounts(); } private FilteredSection FilterSection(JournalData.Section section, FilterConfiguration filter) { IEnumerable source = ((!IsCategorySectionGenreMatch(filter, section.Name)) ? section.Categories.Select((JournalData.Category category) => FilterCategory(category, filter)) : section.Categories.Select((JournalData.Category x) => FilterCategory(x, filter.WithoutName()))); return new FilteredSection(section, source.Where((FilteredCategory x) => x.Genres.Count > 0).ToList()); } private FilteredCategory FilterCategory(JournalData.Category category, FilterConfiguration filter) { IEnumerable source = ((!IsCategorySectionGenreMatch(filter, category.Name)) ? category.Genres.Select((JournalData.Genre genre) => FilterGenre(genre, filter)) : category.Genres.Select((JournalData.Genre x) => FilterGenre(x, filter.WithoutName()))); return new FilteredCategory(category, source.Where((FilteredGenre x) => x.Quests.Count > 0).ToList()); } private FilteredGenre FilterGenre(JournalData.Genre genre, FilterConfiguration filter) { IEnumerable source = ((!IsCategorySectionGenreMatch(filter, genre.Name)) ? genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter, x)) : genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter.WithoutName(), x))); return new FilteredGenre(genre, source.ToList()); } internal void RefreshCounts() { _genreCounts.Clear(); _categoryCounts.Clear(); _sectionCounts.Clear(); foreach (JournalData.Genre genre in _journalData.Genres) { Quest quest; int available = genre.Quests.Count((IQuestInfo x) => _questRegistry.TryGetQuest(x.QuestId, out quest) && !quest.Root.Disabled && !_questFunctions.IsQuestRemoved(x.QuestId)); int total = genre.Quests.Count((IQuestInfo x) => !_questFunctions.IsQuestRemoved(x.QuestId)); int obtainable = genre.Quests.Count((IQuestInfo x) => !_questFunctions.IsQuestUnobtainable(x.QuestId)); int completed = genre.Quests.Count((IQuestInfo x) => _questFunctions.IsQuestComplete(x.QuestId)); _genreCounts[genre] = new JournalCounts(available, total, obtainable, completed); } foreach (JournalData.Category category in _journalData.Categories) { List source = (from x in _genreCounts where category.Genres.Contains(x.Key) select x.Value).ToList(); int available2 = source.Sum((JournalCounts x) => x.Available); int total2 = source.Sum((JournalCounts x) => x.Total); int obtainable2 = source.Sum((JournalCounts x) => x.Obtainable); int completed2 = source.Sum((JournalCounts x) => x.Completed); _categoryCounts[category] = new JournalCounts(available2, total2, obtainable2, completed2); } foreach (JournalData.Section section in _journalData.Sections) { List source2 = (from x in _categoryCounts where section.Categories.Contains(x.Key) select x.Value).ToList(); int available3 = source2.Sum((JournalCounts x) => x.Available); int total3 = source2.Sum((JournalCounts x) => x.Total); int obtainable3 = source2.Sum((JournalCounts x) => x.Obtainable); int completed3 = source2.Sum((JournalCounts x) => x.Completed); _sectionCounts[section] = new JournalCounts(available3, total3, obtainable3, completed3); } } internal void ClearCounts(int type, int code) { foreach (KeyValuePair item in _genreCounts.ToList()) { _genreCounts[item.Key] = item.Value with { Completed = 0 }; } foreach (KeyValuePair item2 in _categoryCounts.ToList()) { _categoryCounts[item2.Key] = item2.Value with { Completed = 0 }; } foreach (KeyValuePair item3 in _sectionCounts.ToList()) { _sectionCounts[item3.Key] = item3.Value with { Completed = 0 }; } } private static bool IsCategorySectionGenreMatch(FilterConfiguration filter, string name) { if (!string.IsNullOrEmpty(filter.SearchText)) { return name.Contains(filter.SearchText, StringComparison.CurrentCultureIgnoreCase); } return true; } private bool IsQuestMatch(FilterConfiguration filter, IQuestInfo questInfo) { if (!string.IsNullOrEmpty(filter.SearchText) && !questInfo.Name.Contains(filter.SearchText, StringComparison.CurrentCultureIgnoreCase) && !(questInfo.QuestId.ToString() == filter.SearchText)) { return false; } if (filter.AvailableOnly && !_questFunctions.IsReadyToAcceptQuest(questInfo.QuestId)) { return false; } if (filter.HideNoPaths && (!_questRegistry.TryGetQuest(questInfo.QuestId, out Quest quest) || quest.Root.Disabled)) { return false; } return true; } }