{ EExpansionVersion.Endwalker, "6.x - Endwalker" },
{ EExpansionVersion.Dawntrail, "7.x - Dawntrail" }
};
+
+ public static string ToFriendlyString(this EExpansionVersion expansionVersion)
+ {
+ return expansionVersion switch
+ {
+ EExpansionVersion.ARealmReborn => "A Realm Reborn",
+ _ => expansionVersion.ToString(),
+ };
+ }
}
using Questionable.Validation;
using Questionable.Validation.Validators;
using Questionable.Windows;
+using Questionable.Windows.JournalComponents;
using Questionable.Windows.QuestComponents;
using Action = Questionable.Controller.Steps.Interactions.Action;
serviceCollection.AddSingleton<QuickAccessButtonsComponent>();
serviceCollection.AddSingleton<RemainingTasksComponent>();
+ serviceCollection.AddSingleton<QuestJournalComponent>();
+ serviceCollection.AddSingleton<GatheringJournalComponent>();
+
serviceCollection.AddSingleton<QuestWindow>();
serviceCollection.AddSingleton<ConfigWindow>();
serviceCollection.AddSingleton<DebugOverlay>();
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using Dalamud.Utility;
+using Dalamud.Utility.Signatures;
+using ImGuiNET;
+using LLib.GameData;
+using Lumina.Excel.GeneratedSheets;
+using Questionable.Controller;
+using Questionable.Model;
+using Questionable.Model.Gathering;
+
+namespace Questionable.Windows.JournalComponents;
+
+internal sealed class GatheringJournalComponent
+{
+ private readonly IDalamudPluginInterface _pluginInterface;
+ private readonly UiUtils _uiUtils;
+ private readonly GatheringPointRegistry _gatheringPointRegistry;
+ private readonly Dictionary<int, string> _gatheringItems;
+ private readonly List<ExpansionPoints> _gatheringPoints;
+ private readonly List<ushort> _gatheredItems = [];
+
+ private delegate byte GetIsGatheringItemGatheredDelegate(ushort item);
+
+ [Signature("48 89 5C 24 ?? 57 48 83 EC 20 8B D9 8B F9")]
+ private GetIsGatheringItemGatheredDelegate _getIsGatheringItemGathered = null!;
+
+ internal bool IsGatheringItemGathered(uint item) => _getIsGatheringItemGathered((ushort)item) != 0;
+
+ public GatheringJournalComponent(IDataManager dataManager, IDalamudPluginInterface pluginInterface, UiUtils uiUtils,
+ IGameInteropProvider gameInteropProvider, GatheringPointRegistry gatheringPointRegistry)
+ {
+ _pluginInterface = pluginInterface;
+ _uiUtils = uiUtils;
+ _gatheringPointRegistry = gatheringPointRegistry;
+ var routeToGatheringPoint = dataManager.GetExcelSheet<GatheringLeveRoute>()!
+ .Where(x => x.UnkData0[0].GatheringPoint != 0)
+ .SelectMany(x => x.UnkData0
+ .Where(y => y.GatheringPoint != 0)
+ .Select(y => new
+ {
+ RouteId = x.RowId,
+ GatheringPointId = y.GatheringPoint
+ }))
+ .GroupBy(x => x.RouteId)
+ .ToDictionary(x => x.Key, x => x.Select(y => y.GatheringPointId).ToList());
+ var gatheringLeveSheet = dataManager.GetExcelSheet<GatheringLeve>()!;
+ var territoryTypeSheet = dataManager.GetExcelSheet<TerritoryType>()!;
+ var gatheringPointToLeve = dataManager.GetExcelSheet<Leve>()!
+ .Where(x => x.RowId > 0)
+ .Select(x =>
+ {
+ uint startZonePlaceName = x.PlaceNameStartZone.Row;
+ startZonePlaceName = startZonePlaceName switch
+ {
+ 27 => 28, // limsa
+ 39 => 52, // gridania
+ 51 => 40, // uldah
+ 62 => 2300, // ishgard
+ _ => startZonePlaceName
+ };
+
+ var territoryType = territoryTypeSheet.FirstOrDefault(y => startZonePlaceName == y.PlaceName.Row)
+ ?? throw new InvalidOperationException($"Unable to use {startZonePlaceName}");
+ return new
+ {
+ LeveId = x.RowId,
+ LeveName = x.Name.ToString(),
+ TerritoryType = (ushort)territoryType.RowId,
+ TerritoryName = territoryType.Name.ToString(),
+ GatheringLeve = gatheringLeveSheet.GetRow((uint)x.DataId),
+ };
+ })
+ .Where(x => x.GatheringLeve != null)
+ .Select(x => new
+ {
+ x.LeveId,
+ x.LeveName,
+ x.TerritoryType,
+ x.TerritoryName,
+ GatheringPoints = x.GatheringLeve!.Route
+ .Where(y => y.Row != 0)
+ .SelectMany(y => routeToGatheringPoint[y.Row]),
+ })
+ .SelectMany(x => x.GatheringPoints.Select(y => new
+ {
+ x.LeveId,
+ x.LeveName,
+ x.TerritoryType,
+ x.TerritoryName,
+ GatheringPointId = y
+ }))
+ .GroupBy(x => x.GatheringPointId)
+ .ToDictionary(x => x.Key, x => x.First());
+
+ var itemSheet = dataManager.GetExcelSheet<Item>()!;
+
+ _gatheringItems = dataManager.GetExcelSheet<GatheringItem>()!
+ .Where(x => x.RowId != 0 && x.GatheringItemLevel.Row != 0)
+ .Select(x => new
+ {
+ GatheringItemId = (int)x.RowId,
+ Name = itemSheet.GetRow((uint)x.Item)?.Name.ToString()
+ })
+ .Where(x => !string.IsNullOrEmpty(x.Name))
+ .ToDictionary(x => x.GatheringItemId, x => x.Name!);
+
+ _gatheringPoints = dataManager.GetExcelSheet<GatheringPoint>()!
+ .Where(x => x.GatheringPointBase.Row != 0)
+ .DistinctBy(x => x.GatheringPointBase.Row)
+ .Select(x => new
+ {
+ GatheringPointId = x.RowId,
+ Point = new DefaultGatheringPoint(new GatheringPointId((ushort)x.GatheringPointBase.Row),
+ x.GatheringPointBase.Value!.GatheringType.Row switch
+ {
+ 0 or 1 => EClassJob.Miner,
+ 2 or 3 => EClassJob.Botanist,
+ _ => EClassJob.Fisher
+ },
+ x.GatheringPointBase.Value.GatheringLevel,
+ x.GatheringPointBase.Value.Item.Where(y => y != 0).Select(y => (ushort)y).ToList(),
+ (EExpansionVersion?)x.TerritoryType.Value?.ExVersion.Row ?? (EExpansionVersion)byte.MaxValue,
+ (ushort)x.TerritoryType.Row,
+ x.TerritoryType.Value?.PlaceName.Value?.Name.ToString(),
+ $"{x.GatheringPointBase.Row} - {x.PlaceName.Value?.Name}")
+ })
+ .Where(x => x.Point.ClassJob != EClassJob.Fisher)
+ .Select(x =>
+ {
+ if (gatheringPointToLeve.TryGetValue((int)x.GatheringPointId, out var leve))
+ {
+ // it's a leve
+ return x.Point with
+ {
+ Expansion = EExpansionVersion.Shadowbringers,
+ TerritoryType = leve.TerritoryType,
+ TerritoryName = leve.TerritoryName,
+ PlaceName = leve.LeveName,
+ };
+ }
+ else if (x.Point.TerritoryType == 1 && _gatheringPointRegistry.TryGetGatheringPoint(x.Point.Id, out GatheringRoot? gatheringRoot))
+ {
+ // for some reason the game doesn't know where this gathering location is
+ var territoryType = territoryTypeSheet.GetRow(gatheringRoot.Steps.Last().TerritoryId)!;
+ return x.Point with
+ {
+ Expansion = (EExpansionVersion)territoryType.ExVersion.Row,
+ TerritoryType = (ushort)territoryType.RowId,
+ TerritoryName = territoryType.PlaceName.Value?.Name.ToString(),
+ };
+ }
+ else
+ return x.Point;
+ })
+ .Where(x => x.Expansion != (EExpansionVersion)byte.MaxValue)
+ .Where(x => x.GatheringItemIds.Count > 0)
+ .GroupBy(x => x.Expansion)
+ .Select(x => new ExpansionPoints(x.Key, x
+ .GroupBy(y => new
+ {
+ y.TerritoryType,
+ TerritoryName = $"{(!string.IsNullOrEmpty(y.TerritoryName) ? y.TerritoryName : "???")} ({y.TerritoryType})"
+ })
+ .Select(y => new TerritoryPoints(y.Key.TerritoryType, y.Key.TerritoryName, y.ToList()))
+ .Where(y => y.Points.Count > 0)
+ .ToList()))
+ .OrderBy(x => x.Expansion)
+ .ToList();
+
+ gameInteropProvider.InitializeFromAttributes(this);
+ }
+
+ public void DrawGatheringItems()
+ {
+ using var tab = ImRaii.TabItem("Gathering Points");
+ if (!tab)
+ return;
+
+ using var table = ImRaii.Table("GatheringPoints", 3, ImGuiTableFlags.NoSavedSettings);
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
+ ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 100 * ImGui.GetIO().FontGlobalScale);
+ ImGui.TableSetupColumn("Collected", ImGuiTableColumnFlags.WidthFixed, 100 * ImGui.GetIO().FontGlobalScale);
+ ImGui.TableHeadersRow();
+
+ foreach (var expansion in _gatheringPoints)
+ DrawExpansion(expansion);
+ }
+
+ private void DrawExpansion(ExpansionPoints expansion)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx(expansion.Expansion.ToFriendlyString(), ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ DrawCount(expansion.CompletedPoints, expansion.TotalPoints);
+ ImGui.TableNextColumn();
+ DrawCount(expansion.CompletedItems, expansion.TotalItems);
+
+ if (open)
+ {
+ foreach (var territory in expansion.PointsByTerritories)
+ DrawTerritory(territory);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawTerritory(TerritoryPoints territory)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx(territory.ToFriendlyString(), ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ DrawCount(territory.CompletedPoints, territory.TotalPoints);
+ ImGui.TableNextColumn();
+ DrawCount(territory.CompletedItems, territory.TotalItems);
+
+ if (open)
+ {
+ foreach (var point in territory.Points)
+ DrawPoint(point);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawPoint(DefaultGatheringPoint point)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx($"{point.PlaceName} ({point.ClassJob} Lv. {point.Level})",
+ ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ float spacing;
+ // ReSharper disable once UnusedVariable
+ using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+ {
+ spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+ }
+
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+ _uiUtils.ChecklistItem(string.Empty, point.IsComplete);
+
+ ImGui.TableNextColumn();
+ DrawCount(point.CompletedItems, point.TotalItems);
+
+ if (open)
+ {
+ foreach (var item in point.GatheringItemIds)
+ DrawItem(item);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawItem(ushort item)
+ {
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TreeNodeEx(_gatheringItems.GetValueOrDefault(item, "???"),
+ ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+
+ ImGui.TableNextColumn();
+ float spacing;
+ // ReSharper disable once UnusedVariable
+ using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+ {
+ spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+ }
+
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+ if (item < 10_000)
+ _uiUtils.ChecklistItem(string.Empty, _gatheredItems.Contains(item));
+ else
+ _uiUtils.ChecklistItem(string.Empty, ImGuiColors.DalamudGrey, FontAwesomeIcon.Minus);
+ }
+
+ private static void DrawCount(int count, int total)
+ {
+ string len = 999.ToString(CultureInfo.CurrentCulture);
+ ImGui.PushFont(UiBuilder.MonoFont);
+
+ string text =
+ $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
+ if (count == total)
+ ImGui.TextColored(ImGuiColors.ParsedGreen, text);
+ else
+ ImGui.TextUnformatted(text);
+
+ ImGui.PopFont();
+ }
+
+ internal void RefreshCounts()
+ {
+ _gatheredItems.Clear();
+ foreach (ushort key in _gatheringItems.Keys)
+ {
+ if (IsGatheringItemGathered(key))
+ _gatheredItems.Add(key);
+ }
+
+ foreach (var expansion in _gatheringPoints)
+ {
+ foreach (var territory in expansion.PointsByTerritories)
+ {
+ foreach (var point in territory.Points)
+ {
+ point.TotalItems = point.GatheringItemIds.Count(x => x < 10_000);
+ point.CompletedItems = point.GatheringItemIds.Count(_gatheredItems.Contains);
+ point.IsComplete = _gatheringPointRegistry.TryGetGatheringPoint(point.Id, out _);
+ }
+
+ territory.TotalItems = territory.Points.Sum(x => x.TotalItems);
+ territory.CompletedItems = territory.Points.Sum(x => x.CompletedItems);
+ territory.CompletedPoints = territory.Points.Count(x => x.IsComplete);
+ }
+
+ expansion.TotalItems = expansion.PointsByTerritories.Sum(x => x.TotalItems);
+ expansion.CompletedItems = expansion.PointsByTerritories.Sum(x => x.CompletedItems);
+ expansion.TotalPoints = expansion.PointsByTerritories.Sum(x => x.TotalPoints);
+ expansion.CompletedPoints = expansion.PointsByTerritories.Sum(x => x.CompletedPoints);
+ }
+ }
+
+ private sealed record ExpansionPoints(EExpansionVersion Expansion, List<TerritoryPoints> PointsByTerritories)
+ {
+ public int TotalItems { get; set; }
+ public int TotalPoints { get; set; }
+ public int CompletedItems { get; set; }
+ public int CompletedPoints { get; set; }
+ }
+
+ private sealed record TerritoryPoints(
+ ushort TerritoryType,
+ string TerritoryName,
+ List<DefaultGatheringPoint> Points)
+ {
+ public int TotalItems { get; set; }
+ public int TotalPoints => Points.Count;
+ public int CompletedItems { get; set; }
+ public int CompletedPoints { get; set; }
+ public string ToFriendlyString() =>
+ !string.IsNullOrEmpty(TerritoryName) ? TerritoryName : $"??? ({TerritoryType})";
+ }
+
+ private sealed record DefaultGatheringPoint(
+ GatheringPointId Id,
+ EClassJob ClassJob,
+ byte Level,
+ List<ushort> GatheringItemIds,
+ EExpansionVersion Expansion,
+ ushort TerritoryType,
+ string? TerritoryName,
+ string? PlaceName)
+ {
+ public int TotalItems { get; set; }
+ public int CompletedItems { get; set; }
+ public bool IsComplete { get; set; }
+ }
+}
--- /dev/null
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+using System.Linq;
+using Dalamud.Interface;
+using Dalamud.Interface.Colors;
+using Dalamud.Interface.Utility.Raii;
+using Dalamud.Plugin;
+using Dalamud.Plugin.Services;
+using ImGuiNET;
+using Questionable.Controller;
+using Questionable.Data;
+using Questionable.Functions;
+using Questionable.Model;
+using Questionable.Windows.QuestComponents;
+
+namespace Questionable.Windows.JournalComponents;
+
+internal sealed class QuestJournalComponent
+{
+ private readonly Dictionary<JournalData.Genre, (int Available, int Completed)> _genreCounts = new();
+ private readonly Dictionary<JournalData.Category, (int Available, int Completed)> _categoryCounts = new();
+ private readonly Dictionary<JournalData.Section, (int Available, int Completed)> _sectionCounts = new();
+
+ 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 ICommandManager _commandManager;
+
+ private List<FilteredSection> _filteredSections = [];
+ private string _searchText = string.Empty;
+
+ public QuestJournalComponent(JournalData journalData, QuestRegistry questRegistry, QuestFunctions questFunctions,
+ UiUtils uiUtils, QuestTooltipComponent questTooltipComponent, IDalamudPluginInterface pluginInterface,
+ ICommandManager commandManager)
+ {
+ _journalData = journalData;
+ _questRegistry = questRegistry;
+ _questFunctions = questFunctions;
+ _uiUtils = uiUtils;
+ _questTooltipComponent = questTooltipComponent;
+ _pluginInterface = pluginInterface;
+ _commandManager = commandManager;
+ }
+
+ public void DrawQuests()
+ {
+ using var tab = ImRaii.TabItem("Quests");
+ if (!tab)
+ 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();
+ }
+
+ ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
+ if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref _searchText, 256))
+ UpdateFilter();
+
+ if (_filteredSections.Count > 0)
+ {
+ using var table = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings);
+ if (!table)
+ return;
+
+ ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
+ ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+ ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
+ ImGui.TableHeadersRow();
+
+ foreach (var section in _filteredSections)
+ DrawSection(section);
+ }
+ else
+ ImGui.Text("No quest or category matches your search text.");
+ }
+
+ private void DrawSection(FilteredSection filter)
+ {
+ if (filter.Section.QuestCount == 0)
+ return;
+
+ (int supported, int completed) = _sectionCounts.GetValueOrDefault(filter.Section);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ DrawCount(supported, filter.Section.QuestCount);
+ ImGui.TableNextColumn();
+ DrawCount(completed, filter.Section.QuestCount);
+
+ if (open)
+ {
+ foreach (var category in filter.Categories)
+ DrawCategory(category);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawCategory(FilteredCategory filter)
+ {
+ if (filter.Category.QuestCount == 0)
+ return;
+
+ (int supported, int completed) = _categoryCounts.GetValueOrDefault(filter.Category);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ DrawCount(supported, filter.Category.QuestCount);
+ ImGui.TableNextColumn();
+ DrawCount(completed, filter.Category.QuestCount);
+
+ if (open)
+ {
+ foreach (var genre in filter.Genres)
+ DrawGenre(genre);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawGenre(FilteredGenre filter)
+ {
+ if (filter.Genre.QuestCount == 0)
+ return;
+
+ (int supported, int completed) = _genreCounts.GetValueOrDefault(filter.Genre);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+
+ bool open = ImGui.TreeNodeEx(filter.Genre.Name, ImGuiTreeNodeFlags.SpanFullWidth);
+
+ ImGui.TableNextColumn();
+ DrawCount(supported, filter.Genre.QuestCount);
+ ImGui.TableNextColumn();
+ DrawCount(completed, filter.Genre.QuestCount);
+
+ if (open)
+ {
+ foreach (var quest in filter.Quests)
+ DrawQuest(quest);
+
+ ImGui.TreePop();
+ }
+ }
+
+ private void DrawQuest(IQuestInfo questInfo)
+ {
+ _questRegistry.TryGetQuest(questInfo.QuestId, out var quest);
+
+ ImGui.TableNextRow();
+ ImGui.TableNextColumn();
+ ImGui.TreeNodeEx(questInfo.Name,
+ ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
+
+
+ if (questInfo is QuestInfo && ImGui.IsItemClicked() &&
+ _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
+ {
+ _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
+ }
+
+ if (ImGui.IsItemHovered())
+ _questTooltipComponent.Draw(questInfo);
+
+ ImGui.TableNextColumn();
+ float spacing;
+ // ReSharper disable once UnusedVariable
+ using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
+ {
+ spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
+ }
+
+ ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
+ _uiUtils.ChecklistItem(string.Empty, quest is { Root.Disabled: false });
+
+ ImGui.TableNextColumn();
+ var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
+ _uiUtils.ChecklistItem(text, color, icon);
+ }
+
+ private static void DrawCount(int count, int total)
+ {
+ string len = 9999.ToString(CultureInfo.CurrentCulture);
+ ImGui.PushFont(UiBuilder.MonoFont);
+
+ string text =
+ $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
+ if (count == total)
+ ImGui.TextColored(ImGuiColors.ParsedGreen, text);
+ else
+ ImGui.TextUnformatted(text);
+
+ ImGui.PopFont();
+ }
+
+ public void UpdateFilter()
+ {
+ Predicate<string> match;
+ if (string.IsNullOrWhiteSpace(_searchText))
+ match = _ => true;
+ else
+ match = x => x.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase);
+
+ _filteredSections = _journalData.Sections
+ .Select(section => FilterSection(section, match))
+ .Where(x => x != null)
+ .Cast<FilteredSection>()
+ .ToList();
+ }
+
+ private static FilteredSection? FilterSection(JournalData.Section section, Predicate<string> match)
+ {
+ if (match(section.Name))
+ {
+ return new FilteredSection(section,
+ section.Categories
+ .Select(x => FilterCategory(x, _ => true))
+ .Cast<FilteredCategory>()
+ .ToList());
+ }
+ else
+ {
+ List<FilteredCategory> filteredCategories = section.Categories
+ .Select(category => FilterCategory(category, match))
+ .Where(x => x != null)
+ .Cast<FilteredCategory>()
+ .ToList();
+ if (filteredCategories.Count > 0)
+ return new FilteredSection(section, filteredCategories);
+
+ return null;
+ }
+ }
+
+ private static FilteredCategory? FilterCategory(JournalData.Category category, Predicate<string> match)
+ {
+ if (match(category.Name))
+ {
+ return new FilteredCategory(category,
+ category.Genres
+ .Select(x => FilterGenre(x, _ => true))
+ .Cast<FilteredGenre>()
+ .ToList());
+ }
+ else
+ {
+ List<FilteredGenre> filteredGenres = category.Genres
+ .Select(genre => FilterGenre(genre, match))
+ .Where(x => x != null)
+ .Cast<FilteredGenre>()
+ .ToList();
+ if (filteredGenres.Count > 0)
+ return new FilteredCategory(category, filteredGenres);
+
+ return null;
+ }
+ }
+
+ private static FilteredGenre? FilterGenre(JournalData.Genre genre, Predicate<string> match)
+ {
+ if (match(genre.Name))
+ return new FilteredGenre(genre, genre.Quests);
+ else
+ {
+ List<IQuestInfo> filteredQuests = genre.Quests
+ .Where(x => match(x.Name))
+ .ToList();
+ if (filteredQuests.Count > 0)
+ return new FilteredGenre(genre, filteredQuests);
+ }
+
+ return null;
+ }
+
+ internal void RefreshCounts()
+ {
+ _genreCounts.Clear();
+ _categoryCounts.Clear();
+ _sectionCounts.Clear();
+
+ foreach (var genre in _journalData.Genres)
+ {
+ int available = genre.Quests.Count(x =>
+ _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
+ int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId));
+ _genreCounts[genre] = (available, completed);
+ }
+
+ foreach (var category in _journalData.Categories)
+ {
+ var counts = _genreCounts
+ .Where(x => category.Genres.Contains(x.Key))
+ .Select(x => x.Value)
+ .ToList();
+ int available = counts.Sum(x => x.Available);
+ int completed = counts.Sum(x => x.Completed);
+ _categoryCounts[category] = (available, completed);
+ }
+
+ foreach (var section in _journalData.Sections)
+ {
+ var counts = _categoryCounts
+ .Where(x => section.Categories.Contains(x.Key))
+ .Select(x => x.Value)
+ .ToList();
+ int available = counts.Sum(x => x.Available);
+ int completed = counts.Sum(x => x.Completed);
+ _sectionCounts[section] = (available, completed);
+ }
+ }
+
+ internal void ClearCounts()
+ {
+ foreach (var genreCount in _genreCounts.ToList())
+ _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0);
+
+ foreach (var categoryCount in _categoryCounts.ToList())
+ _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0);
+
+ foreach (var sectionCount in _sectionCounts.ToList())
+ _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0);
+ }
+
+ 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);
+}
using Questionable.Data;
using Questionable.Functions;
using Questionable.Model;
+using Questionable.Windows.JournalComponents;
using Questionable.Windows.QuestComponents;
namespace Questionable.Windows;
internal sealed class JournalProgressWindow : LWindow, IDisposable
{
- private readonly JournalData _journalData;
+ private readonly QuestJournalComponent _questJournalComponent;
+ private readonly GatheringJournalComponent _gatheringJournalComponent;
private readonly QuestRegistry _questRegistry;
- private readonly QuestFunctions _questFunctions;
- private readonly UiUtils _uiUtils;
- private readonly QuestTooltipComponent _questTooltipComponent;
- private readonly IDalamudPluginInterface _pluginInterface;
private readonly IClientState _clientState;
- private readonly ICommandManager _commandManager;
- private readonly Dictionary<JournalData.Genre, (int Available, int Completed)> _genreCounts = new();
- private readonly Dictionary<JournalData.Category, (int Available, int Completed)> _categoryCounts = new();
- private readonly Dictionary<JournalData.Section, (int Available, int Completed)> _sectionCounts = new();
-
- private List<FilteredSection> _filteredSections = [];
- private string _searchText = string.Empty;
-
- public JournalProgressWindow(JournalData journalData,
+ public JournalProgressWindow(
+ QuestJournalComponent questJournalComponent,
+ GatheringJournalComponent gatheringJournalComponent,
QuestRegistry questRegistry,
- QuestFunctions questFunctions,
- UiUtils uiUtils,
- QuestTooltipComponent questTooltipComponent,
- IDalamudPluginInterface pluginInterface,
- IClientState clientState,
- ICommandManager commandManager)
+ IClientState clientState)
: base("Journal Progress###QuestionableJournalProgress")
{
- _journalData = journalData;
+ _questJournalComponent = questJournalComponent;
+ _gatheringJournalComponent = gatheringJournalComponent;
_questRegistry = questRegistry;
- _questFunctions = questFunctions;
- _uiUtils = uiUtils;
- _questTooltipComponent = questTooltipComponent;
- _pluginInterface = pluginInterface;
_clientState = clientState;
- _commandManager = commandManager;
- _clientState.Login += RefreshCounts;
- _clientState.Logout -= ClearCounts;
+ _clientState.Login += _questJournalComponent.RefreshCounts;
+ _clientState.Login += _gatheringJournalComponent.RefreshCounts;
+ _clientState.Logout -= _questJournalComponent.ClearCounts;
_questRegistry.Reloaded += OnQuestsReloaded;
SizeConstraints = new WindowSizeConstraints
};
}
- private void OnQuestsReloaded(object? sender, EventArgs e) => RefreshCounts();
-
- public override void OnOpen()
+ private void OnQuestsReloaded(object? sender, EventArgs e)
{
- UpdateFilter();
- RefreshCounts();
+ _questJournalComponent.RefreshCounts();
+ _gatheringJournalComponent.RefreshCounts();
}
- public override void Draw()
- {
- 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();
- }
-
- ImGui.SetNextItemWidth(ImGui.GetContentRegionAvail().X);
- if (ImGui.InputTextWithHint(string.Empty, "Search quests and categories", ref _searchText, 256))
- UpdateFilter();
-
- if (_filteredSections.Count > 0)
- {
- using var table = ImRaii.Table("Quests", 3, ImGuiTableFlags.NoSavedSettings);
- if (!table)
- return;
-
- ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide);
- ImGui.TableSetupColumn("Supported", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
- ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120 * ImGui.GetIO().FontGlobalScale);
- ImGui.TableHeadersRow();
-
- foreach (var section in _filteredSections)
- {
- DrawSection(section);
- }
- }
- else
- ImGui.Text("No quest or category matches your search text.");
- }
-
- private void DrawSection(FilteredSection filter)
- {
- if (filter.Section.QuestCount == 0)
- return;
-
- (int supported, int completed) = _sectionCounts.GetValueOrDefault(filter.Section);
-
- ImGui.TableNextRow();
- ImGui.TableNextColumn();
-
- bool open = ImGui.TreeNodeEx(filter.Section.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
- ImGui.TableNextColumn();
- DrawCount(supported, filter.Section.QuestCount);
- ImGui.TableNextColumn();
- DrawCount(completed, filter.Section.QuestCount);
-
- if (open)
- {
- foreach (var category in filter.Categories)
- DrawCategory(category);
-
- ImGui.TreePop();
- }
- }
-
- private void DrawCategory(FilteredCategory filter)
+ public override void OnOpen()
{
- if (filter.Category.QuestCount == 0)
- return;
-
- (int supported, int completed) = _categoryCounts.GetValueOrDefault(filter.Category);
-
- ImGui.TableNextRow();
- ImGui.TableNextColumn();
-
- bool open = ImGui.TreeNodeEx(filter.Category.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
- ImGui.TableNextColumn();
- DrawCount(supported, filter.Category.QuestCount);
- ImGui.TableNextColumn();
- DrawCount(completed, filter.Category.QuestCount);
-
- if (open)
- {
- foreach (var genre in filter.Genres)
- DrawGenre(genre);
-
- ImGui.TreePop();
- }
+ _questJournalComponent.UpdateFilter();
+ _questJournalComponent.RefreshCounts();
+ _gatheringJournalComponent.RefreshCounts();
}
- private void DrawGenre(FilteredGenre filter)
+ public override void Draw()
{
- if (filter.Genre.QuestCount == 0)
+ using var tabBar = ImRaii.TabBar("Journal");
+ if (!tabBar)
return;
- (int supported, int completed) = _genreCounts.GetValueOrDefault(filter.Genre);
-
- ImGui.TableNextRow();
- ImGui.TableNextColumn();
-
- bool open = ImGui.TreeNodeEx(filter.Genre.Name, ImGuiTreeNodeFlags.SpanFullWidth);
-
- ImGui.TableNextColumn();
- DrawCount(supported, filter.Genre.QuestCount);
- ImGui.TableNextColumn();
- DrawCount(completed, filter.Genre.QuestCount);
-
- if (open)
- {
- foreach (var quest in filter.Quests)
- DrawQuest(quest);
-
- ImGui.TreePop();
- }
- }
-
- private void DrawQuest(IQuestInfo questInfo)
- {
- _questRegistry.TryGetQuest(questInfo.QuestId, out var quest);
-
- ImGui.TableNextRow();
- ImGui.TableNextColumn();
- ImGui.TreeNodeEx(questInfo.Name,
- ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.SpanFullWidth);
-
-
- if (questInfo is QuestInfo && ImGui.IsItemClicked() && _commandManager.Commands.TryGetValue("/questinfo", out var commandInfo))
- {
- _commandManager.DispatchCommand("/questinfo", questInfo.QuestId.ToString() ?? string.Empty, commandInfo);
- }
-
- if (ImGui.IsItemHovered())
- _questTooltipComponent.Draw(questInfo);
-
- ImGui.TableNextColumn();
- float spacing;
- // ReSharper disable once UnusedVariable
- using (var font = _pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push())
- {
- spacing = ImGui.GetColumnWidth() / 2 - ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X;
- }
-
- ImGui.SetCursorPosX(ImGui.GetCursorPosX() + spacing);
- _uiUtils.ChecklistItem(string.Empty, quest is { Root.Disabled: false });
-
- ImGui.TableNextColumn();
- var (color, icon, text) = _uiUtils.GetQuestStyle(questInfo.QuestId);
- _uiUtils.ChecklistItem(text, color, icon);
- }
-
- private static void DrawCount(int count, int total)
- {
- string len = 9999.ToString(CultureInfo.CurrentCulture);
- ImGui.PushFont(UiBuilder.MonoFont);
-
- string text =
- $"{count.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)} / {total.ToString(CultureInfo.CurrentCulture).PadLeft(len.Length)}";
- if (count == total)
- ImGui.TextColored(ImGuiColors.ParsedGreen, text);
- else
- ImGui.TextUnformatted(text);
-
- ImGui.PopFont();
- }
-
- private void UpdateFilter()
- {
- Predicate<string> match;
- if (string.IsNullOrWhiteSpace(_searchText))
- match = _ => true;
- else
- match = x => x.Contains(_searchText, StringComparison.CurrentCultureIgnoreCase);
-
- _filteredSections = _journalData.Sections
- .Select(section => FilterSection(section, match))
- .Where(x => x != null)
- .Cast<FilteredSection>()
- .ToList();
- }
-
- private static FilteredSection? FilterSection(JournalData.Section section, Predicate<string> match)
- {
- if (match(section.Name))
- {
- return new FilteredSection(section,
- section.Categories
- .Select(x => FilterCategory(x, _ => true))
- .Cast<FilteredCategory>()
- .ToList());
- }
- else
- {
- List<FilteredCategory> filteredCategories = section.Categories
- .Select(category => FilterCategory(category, match))
- .Where(x => x != null)
- .Cast<FilteredCategory>()
- .ToList();
- if (filteredCategories.Count > 0)
- return new FilteredSection(section, filteredCategories);
-
- return null;
- }
- }
-
- private static FilteredCategory? FilterCategory(JournalData.Category category, Predicate<string> match)
- {
- if (match(category.Name))
- {
- return new FilteredCategory(category,
- category.Genres
- .Select(x => FilterGenre(x, _ => true))
- .Cast<FilteredGenre>()
- .ToList());
- }
- else
- {
- List<FilteredGenre> filteredGenres = category.Genres
- .Select(genre => FilterGenre(genre, match))
- .Where(x => x != null)
- .Cast<FilteredGenre>()
- .ToList();
- if (filteredGenres.Count > 0)
- return new FilteredCategory(category, filteredGenres);
-
- return null;
- }
- }
-
- private static FilteredGenre? FilterGenre(JournalData.Genre genre, Predicate<string> match)
- {
- if (match(genre.Name))
- return new FilteredGenre(genre, genre.Quests);
- else
- {
- List<IQuestInfo> filteredQuests = genre.Quests
- .Where(x => match(x.Name))
- .ToList();
- if (filteredQuests.Count > 0)
- return new FilteredGenre(genre, filteredQuests);
- }
-
- return null;
- }
-
- private void RefreshCounts()
- {
- _genreCounts.Clear();
- _categoryCounts.Clear();
- _sectionCounts.Clear();
-
- foreach (var genre in _journalData.Genres)
- {
- int available = genre.Quests.Count(x =>
- _questRegistry.TryGetQuest(x.QuestId, out var quest) && !quest.Root.Disabled);
- int completed = genre.Quests.Count(x => _questFunctions.IsQuestComplete(x.QuestId));
- _genreCounts[genre] = (available, completed);
- }
-
- foreach (var category in _journalData.Categories)
- {
- var counts = _genreCounts
- .Where(x => category.Genres.Contains(x.Key))
- .Select(x => x.Value)
- .ToList();
- int available = counts.Sum(x => x.Available);
- int completed = counts.Sum(x => x.Completed);
- _categoryCounts[category] = (available, completed);
- }
-
- foreach (var section in _journalData.Sections)
- {
- var counts = _categoryCounts
- .Where(x => section.Categories.Contains(x.Key))
- .Select(x => x.Value)
- .ToList();
- int available = counts.Sum(x => x.Available);
- int completed = counts.Sum(x => x.Completed);
- _sectionCounts[section] = (available, completed);
- }
- }
-
- private void ClearCounts()
- {
- foreach (var genreCount in _genreCounts.ToList())
- _genreCounts[genreCount.Key] = (genreCount.Value.Available, 0);
-
- foreach (var categoryCount in _categoryCounts.ToList())
- _categoryCounts[categoryCount.Key] = (categoryCount.Value.Available, 0);
-
- foreach (var sectionCount in _sectionCounts.ToList())
- _sectionCounts[sectionCount.Key] = (sectionCount.Value.Available, 0);
+ _questJournalComponent.DrawQuests();
+ _gatheringJournalComponent.DrawGatheringItems();
}
public void Dispose()
{
_questRegistry.Reloaded -= OnQuestsReloaded;
- _clientState.Logout -= ClearCounts;
- _clientState.Login -= RefreshCounts;
+ _clientState.Logout -= _questJournalComponent.ClearCounts;
+ _clientState.Login -= _gatheringJournalComponent.RefreshCounts;
+ _clientState.Login -= _questJournalComponent.RefreshCounts;
}
-
- 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);
}