qstbak/Questionable/Questionable.Windows.JournalComponents/QuestJournalComponent.cs
2025-10-10 10:10:09 +10:00

555 lines
18 KiB
C#

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.Components;
using Dalamud.Interface.Utility.Raii;
using Dalamud.Plugin;
using Dalamud.Utility;
using Microsoft.Extensions.Logging;
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<FilteredCategory> Categories);
private sealed record FilteredCategory(JournalData.Category Category, List<FilteredGenre> Genres);
private sealed record FilteredGenre(JournalData.Genre Genre, List<IQuestInfo> 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 HideUnobtainable;
public bool AdvancedFiltersActive
{
get
{
if (!AvailableOnly && !HideNoPaths)
{
return HideUnobtainable;
}
return true;
}
}
public FilterConfiguration WithoutName()
{
return new FilterConfiguration
{
AvailableOnly = AvailableOnly,
HideNoPaths = HideNoPaths,
HideUnobtainable = HideUnobtainable
};
}
}
private readonly Dictionary<JournalData.Genre, JournalCounts> _genreCounts = new Dictionary<JournalData.Genre, JournalCounts>();
private readonly Dictionary<JournalData.Category, JournalCounts> _categoryCounts = new Dictionary<JournalData.Category, JournalCounts>();
private readonly Dictionary<JournalData.Section, JournalCounts> _sectionCounts = new Dictionary<JournalData.Section, JournalCounts>();
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 readonly Configuration _configuration;
private readonly ILogger<QuestJournalComponent> _logger;
private const uint SeasonalJournalCategoryRowId = 96u;
private List<FilteredSection> _filteredSections = new List<FilteredSection>();
private bool _lastHideSeasonalGlobally;
internal FilterConfiguration Filter { get; } = new FilterConfiguration();
public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions, UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface, QuestJournalUtils questJournalUtils, QuestValidator questValidator, Configuration configuration, ILogger<QuestJournalComponent> logger)
{
_journalData = journalData;
_questRegistry = questRegistry;
_questFunctions = questFunctions;
_uiUtils = uiUtils;
_questTooltipComponent = questTooltipComponent;
_pluginInterface = pluginInterface;
_questJournalUtils = questJournalUtils;
_questValidator = questValidator;
_configuration = configuration;
_logger = logger;
_lastHideSeasonalGlobally = _configuration.General.HideSeasonalEventsFromJournalProgress;
}
public void DrawQuests()
{
using ImRaii.IEndObject endObject = ImRaii.TabItem("Quests");
if (!endObject)
{
return;
}
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
if (hideSeasonalEventsFromJournalProgress != _lastHideSeasonalGlobally)
{
_lastHideSeasonalGlobally = hideSeasonalEventsFromJournalProgress;
_logger.LogDebug("Configuration change detected: HideSeasonalEventsFromJournalProgress={Hide} - refreshing journal", hideSeasonalEventsFromJournalProgress);
UpdateFilter();
}
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 or past seasonal events.");
ImGui.Spacing();
ImGui.Separator();
ImGui.Spacing();
}
QuestJournalUtils.ShowFilterContextMenu(this);
ImGui.SameLine();
if (ImGuiComponents.IconButton(FontAwesomeIcon.GlobeEurope))
{
Util.OpenLink("https://wigglymuffin.github.io/FFXIV-Tools/");
}
if (ImGui.IsItemHovered())
{
ImGui.SetTooltip("View All Quest Paths Online");
}
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<ValidationIssue> 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_0210;
}
}
_uiUtils.ChecklistItem(string.Empty, complete: false);
}
goto IL_0210;
IL_0210:
ImGui.TableNextColumn();
if (_questFunctions.IsQuestAccepted(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Active", ImGuiColors.DalamudYellow, FontAwesomeIcon.PersonWalkingArrowRight);
return;
}
if (_questFunctions.IsQuestComplete(questInfo.QuestId))
{
if (questInfo.IsRepeatable && _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Complete", ImGuiColors.ParsedBlue, FontAwesomeIcon.Check);
}
else
{
_uiUtils.ChecklistItem("Complete", ImGuiColors.ParsedGreen, FontAwesomeIcon.Check);
}
return;
}
bool flag = false;
bool flag2 = _questFunctions.IsQuestUnobtainable(questInfo.QuestId);
bool flag3 = _questFunctions.IsQuestLocked(questInfo.QuestId);
bool flag4 = _questFunctions.IsReadyToAcceptQuest(questInfo.QuestId);
DateTime? seasonalQuestExpiry = questInfo.SeasonalQuestExpiry;
if (seasonalQuestExpiry.HasValue)
{
DateTime valueOrDefault = seasonalQuestExpiry.GetValueOrDefault();
DateTime dateTime = ((valueOrDefault.Kind == DateTimeKind.Utc) ? valueOrDefault : valueOrDefault.ToUniversalTime());
if (DateTime.UtcNow > dateTime)
{
flag = true;
}
}
if (flag || flag2)
{
_uiUtils.ChecklistItem("Unobtainable", ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
}
else if (flag3 || !flag4 || !_questRegistry.IsKnownQuest(questInfo.QuestId))
{
_uiUtils.ChecklistItem("Locked", ImGuiColors.DalamudRed, FontAwesomeIcon.Times);
}
else
{
_uiUtils.ChecklistItem("Available", ImGuiColors.DalamudYellow, FontAwesomeIcon.Running);
}
}
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<JournalData.Category> enumerable;
if (!_configuration.General.HideSeasonalEventsFromJournalProgress)
{
IEnumerable<JournalData.Category> categories = section.Categories;
enumerable = categories;
}
else
{
enumerable = section.Categories.Where((JournalData.Category c) => c.Id != 96);
}
IEnumerable<JournalData.Category> source = enumerable;
return new FilteredSection(Categories: ((!IsCategorySectionGenreMatch(filter, section.Name)) ? source.Select((JournalData.Category category) => FilterCategory(category, filter, section)) : source.Select((JournalData.Category x) => FilterCategory(x, filter.WithoutName(), section))).Where((FilteredCategory x) => x.Genres.Count > 0).ToList(), Section: section);
}
private FilteredCategory FilterCategory(JournalData.Category category, FilterConfiguration filter, JournalData.Section? parentSection = null)
{
IEnumerable<FilteredGenre> source = ((!IsCategorySectionGenreMatch(filter, category.Name)) ? category.Genres.Select((JournalData.Genre genre) => FilterGenre(genre, filter, parentSection)) : category.Genres.Select((JournalData.Genre x) => FilterGenre(x, filter.WithoutName(), parentSection)));
return new FilteredCategory(category, source.Where((FilteredGenre x) => x.Quests.Count > 0).ToList());
}
private FilteredGenre FilterGenre(JournalData.Genre genre, FilterConfiguration filter, JournalData.Section? parentSection = null)
{
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
IEnumerable<IQuestInfo> source = ((!IsCategorySectionGenreMatch(filter, genre.Name)) ? genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter, x)) : genre.Quests.Where((IQuestInfo x) => IsQuestMatch(filter.WithoutName(), x)));
if (hideSeasonalEventsFromJournalProgress && genre.CategoryId == 96)
{
source = source.Where((IQuestInfo q) => !IsSeasonal(q));
}
return new FilteredGenre(genre, source.ToList());
}
internal void RefreshCounts()
{
_genreCounts.Clear();
_categoryCounts.Clear();
_sectionCounts.Clear();
bool hideSeasonalEventsFromJournalProgress = _configuration.General.HideSeasonalEventsFromJournalProgress;
_logger.LogInformation("Refreshing journal counts. HideSeasonalEventsFromJournalProgress={Hide}", hideSeasonalEventsFromJournalProgress);
foreach (JournalData.Genre genre in _journalData.Genres)
{
List<IQuestInfo> source = ((hideSeasonalEventsFromJournalProgress && genre.CategoryId == 96) ? genre.Quests.Where((IQuestInfo q) => !IsSeasonal(q)).ToList() : genre.Quests.ToList());
Quest quest;
int available = source.Count((IQuestInfo x) => _questRegistry.TryGetQuest(x.QuestId, out quest) && !quest.Root.Disabled && !_questFunctions.IsQuestRemoved(x.QuestId));
int total = source.Count((IQuestInfo x) => !_questFunctions.IsQuestRemoved(x.QuestId));
int obtainable = source.Count((IQuestInfo x) => !_questFunctions.IsQuestUnobtainable(x.QuestId));
int completed = source.Count((IQuestInfo x) => _questFunctions.IsQuestComplete(x.QuestId));
_genreCounts[genre] = new JournalCounts(available, total, obtainable, completed);
}
foreach (JournalData.Category category in _journalData.Categories)
{
if (!hideSeasonalEventsFromJournalProgress || category.Id != 96)
{
List<JournalCounts> source2 = _genre_counts_or_default(category);
int available2 = source2.Sum((JournalCounts x) => x.Available);
int total2 = source2.Sum((JournalCounts x) => x.Total);
int obtainable2 = source2.Sum((JournalCounts x) => x.Obtainable);
int completed2 = source2.Sum((JournalCounts x) => x.Completed);
_categoryCounts[category] = new JournalCounts(available2, total2, obtainable2, completed2);
}
}
foreach (JournalData.Section section in _journalData.Sections)
{
List<JournalCounts> source3 = (from x in _categoryCounts
where section.Categories.Contains(x.Key)
select x.Value).ToList();
int available3 = source3.Sum((JournalCounts x) => x.Available);
int total3 = source3.Sum((JournalCounts x) => x.Total);
int obtainable3 = source3.Sum((JournalCounts x) => x.Obtainable);
int completed3 = source3.Sum((JournalCounts x) => x.Completed);
_sectionCounts[section] = new JournalCounts(available3, total3, obtainable3, completed3);
}
int num = _sectionCounts.Values.Sum((JournalCounts x) => x.Total);
_logger.LogDebug("RefreshCounts complete. Sections={Sections}, Categories={Categories}, Genres={Genres}, TotalQuests={Total}", _sectionCounts.Count, _categoryCounts.Count, _genreCounts.Count, num);
}
private List<JournalCounts> _genre_counts_or_default(JournalData.Category category)
{
return (from x in _genreCounts
where category.Genres.Contains(x.Key)
select x.Value).ToList();
}
internal void ClearCounts(int type, int code)
{
foreach (KeyValuePair<JournalData.Genre, JournalCounts> item in _genreCounts.ToList())
{
_genreCounts[item.Key] = item.Value with
{
Completed = 0
};
}
foreach (KeyValuePair<JournalData.Category, JournalCounts> item2 in _categoryCounts.ToList())
{
_categoryCounts[item2.Key] = item2.Value with
{
Completed = 0
};
}
foreach (KeyValuePair<JournalData.Section, JournalCounts> 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;
}
if (filter.HideUnobtainable && _questFunctions.IsQuestUnobtainable(questInfo.QuestId))
{
return false;
}
return true;
}
private static bool IsSeasonal(IQuestInfo q)
{
if (q == null)
{
return false;
}
if (q.IsSeasonalQuest)
{
return true;
}
if (q.SeasonalQuestExpiry.HasValue)
{
return true;
}
if (q is UnlockLinkQuestInfo { QuestExpiry: not null })
{
return true;
}
return false;
}
}