using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.CompilerServices; using Dalamud.Bindings.ImGui; using Dalamud.Interface; using Dalamud.Interface.Utility.Raii; using Dalamud.Plugin; using Microsoft.Extensions.Logging; using Questionable.Data; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Windows.JournalComponents; internal sealed class DutyJournalComponent { private enum RowType : byte { Category, Duty } private readonly record struct FlatRow(RowType Type, int GroupIndex, DutyInfo? Duty, string Label, DutyCounts Counts); private sealed record DutyCategory(string Name, List Duties); private sealed record DutyCounts(int Unlocked, int Completed, int CompletionTrackable, int Total) { public string FormattedUnlocked { get; } = FormatPair(Unlocked, Total); public string FormattedCompleted { get; } = FormatPair(Completed, CompletionTrackable); public bool IsUnlockedComplete { get { if (Unlocked == Total) { return Total > 0; } return false; } } public bool IsCompletedComplete { get { if (Completed == CompletionTrackable) { return CompletionTrackable > 0; } return false; } } private static readonly int PadWidth = 9999.ToString(CultureInfo.CurrentCulture).Length; public DutyCounts() : this(0, 0, 0, 0) { } private static string FormatPair(int n, int d) { if (d != 0) { return n.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth) + " / " + d.ToString(CultureInfo.CurrentCulture).PadLeft(PadWidth); } return "-".PadLeft(PadWidth) + " / " + "-".PadLeft(PadWidth); } [CompilerGenerated] private DutyCounts(DutyCounts original) { Unlocked = original.Unlocked; Completed = original.Completed; CompletionTrackable = original.CompletionTrackable; Total = original.Total; FormattedUnlocked = original.FormattedUnlocked; FormattedCompleted = original.FormattedCompleted; } } private readonly DutyUnlockData _dutyUnlockData; private readonly QuestData _questData; private readonly QuestFunctions _questFunctions; private readonly UiUtils _uiUtils; private readonly IDalamudPluginInterface _pluginInterface; private readonly ILogger _logger; private List _filteredCategories = new List(); private Dictionary _categoryCounts = new Dictionary(); private List _flatRows = new List(); private readonly HashSet _expandedGroups = new HashSet(); private string _searchText = string.Empty; private bool _showLockedOnly; private bool _showUnlockedOnly; public DutyJournalComponent(DutyUnlockData dutyUnlockData, QuestData questData, QuestFunctions questFunctions, UiUtils uiUtils, IDalamudPluginInterface pluginInterface, ILogger logger) { _dutyUnlockData = dutyUnlockData; _questData = questData; _questFunctions = questFunctions; _uiUtils = uiUtils; _pluginInterface = pluginInterface; _logger = logger; } public unsafe void DrawDuties() { DrawFilterControls(); ImGui.Separator(); if (_flatRows.Count > 0) { float x; using (_pluginInterface.UiBuilder.IconFontFixedWidthHandle.Push()) { x = ImGui.CalcTextSize(FontAwesomeIcon.Check.ToIconString()).X; } float fontGlobalScale = ImGui.GetIO().FontGlobalScale; using ImRaii.TableDisposable tableDisposable = UiThemeUtils.BeginThemedTable("Duties", 3, ImGuiTableFlags.NoSavedSettings | ImGuiTableFlags.ScrollY); if (!tableDisposable) { return; } ImGui.TableSetupColumn("Name", ImGuiTableColumnFlags.NoHide); ImGui.TableSetupColumn("Unlocked", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale); ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 120f * fontGlobalScale); ImGui.TableHeadersRow(); float textLineHeightWithSpacing = ImGui.GetTextLineHeightWithSpacing(); bool flag = false; ImGuiListClipper* intPtr = ImGuiNative.ImGuiListClipper(); ImGuiNative.Begin(intPtr, _flatRows.Count, textLineHeightWithSpacing); ImGuiListClipperPtr imGuiListClipperPtr = intPtr; try { while (imGuiListClipperPtr.Step()) { for (int i = imGuiListClipperPtr.DisplayStart; i < imGuiListClipperPtr.DisplayEnd; i++) { if (DrawFlatRow(_flatRows[i], x)) { flag = true; } } } } finally { imGuiListClipperPtr.Destroy(); } if (flag) { RebuildFlatRows(); } return; } UiThemeUtils.EmptyState(FontAwesomeIcon.Dungeon, "No duties match your search."); } private bool DrawFlatRow(FlatRow row, float statusIconSpacing) { ImGui.TableNextRow(); ImGui.TableNextColumn(); bool result = false; if (row.Type == RowType.Duty) { float indentSpacing = ImGui.GetStyle().IndentSpacing; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + indentSpacing); string text = $"Lv{row.Duty.Level} {row.Duty.Name}"; if (row.Duty.ItemLevel > 0) { text += $" (i{row.Duty.ItemLevel})"; } ImGui.TreeNodeEx(text, ImGuiTreeNodeFlags.NoTreePushOnOpen | ImGuiTreeNodeFlags.Leaf | ImGuiTreeNodeFlags.SpanFullWidth); if (ImGui.IsItemHovered()) { DrawDutyTooltip(row.Duty); } ImGui.TableNextColumn(); DrawDutyStatus(row.Duty.IsUnlocked, statusIconSpacing); ImGui.TableNextColumn(); DrawDutyStatus(row.Duty.IsCompleted, statusIconSpacing); } else { bool flag = _expandedGroups.Contains(row.Label); UiThemeUtils.DrawTrackerGroupCaret(_pluginInterface.UiBuilder.IconFontFixedWidthHandle, flag); ImGui.SameLine(); ImU8String label = new ImU8String(5, 2); label.AppendFormatted(row.Label); label.AppendLiteral("##grp"); label.AppendFormatted(row.GroupIndex); if (ImGui.Selectable(label)) { if (flag) { _expandedGroups.Remove(row.Label); } else { _expandedGroups.Add(row.Label); } result = true; } ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.Counts.FormattedUnlocked, row.Counts.IsUnlockedComplete, row.Counts.Total == 0); ImGui.TableNextColumn(); UiThemeUtils.DrawTrackerCount(row.Counts.FormattedCompleted, row.Counts.IsCompletedComplete, row.Counts.CompletionTrackable == 0); } return result; } private void DrawFilterControls() { float x = ImGui.GetStyle().ItemSpacing.X; float num = 2f * ImGui.GetFrameHeight() + ImGui.CalcTextSize("Locked Only").X + ImGui.CalcTextSize("Unlocked Only").X + 5f * x; if (UiThemeUtils.SearchInput("##DutySearch", ref _searchText, 0f - num, "Search duties...")) { UpdateFilter(); } ImGui.SameLine(); if (UiThemeUtils.WrappedCheckbox("Locked Only", ref _showLockedOnly)) { if (_showLockedOnly) { _showUnlockedOnly = false; } UpdateFilter(); } ImGui.SameLine(); if (UiThemeUtils.WrappedCheckbox("Unlocked Only", ref _showUnlockedOnly)) { if (_showUnlockedOnly) { _showLockedOnly = false; } UpdateFilter(); } } private void DrawDutyTooltip(DutyInfo duty) { using ImRaii.TooltipDisposable tooltipDisposable = ImRaii.Tooltip(); if (!tooltipDisposable.Alive) { return; } ImGui.TextColored(in UiThemeUtils.PrimaryTextColor, duty.Name); ImU8String text = new ImU8String(12, 1); text.AppendLiteral("Content ID: "); text.AppendFormatted(duty.ContentFinderConditionId); ImGui.TextColored(in UiThemeUtils.DimTextColor, text); ImGui.Separator(); ImU8String text2 = new ImU8String(16, 1); text2.AppendLiteral("Level Required: "); text2.AppendFormatted(duty.Level); ImGui.Text(text2); if (duty.ItemLevel > 0) { ImU8String text3 = new ImU8String(21, 1); text3.AppendLiteral("Item Level Required: "); text3.AppendFormatted(duty.ItemLevel); ImGui.Text(text3); } ImU8String text4 = new ImU8String(6, 1); text4.AppendLiteral("Type: "); text4.AppendFormatted(duty.ContentTypeName); ImGui.Text(text4); if (duty.IsHighEndDuty) { ImGui.SameLine(); ImGui.TextColored(in UiThemeUtils.StatusActive, "(High-End)"); } ImGui.Separator(); if (duty.IsUnlocked) { ImGui.TextColored(in UiThemeUtils.StatusComplete, "Status: Unlocked"); } else { ImGui.TextColored(in UiThemeUtils.StatusLocked, "Status: Locked"); } if (duty.UnlockQuests.Count > 0) { ImGui.Separator(); ImGui.Text("Unlock Quest(s):"); { foreach (QuestId unlockQuest in duty.UnlockQuests) { if (_questData.TryGetQuestInfo(unlockQuest, out IQuestInfo questInfo)) { var (color, icon, _) = _uiUtils.GetQuestStyle(unlockQuest); _uiUtils.ChecklistItem($"{questInfo.Name} ({unlockQuest})", color, icon); } else { _uiUtils.ChecklistItem($"Unknown Quest ({unlockQuest})", UiThemeUtils.StatusUnobtainable, FontAwesomeIcon.Question); } } return; } } ImGui.Separator(); ImGui.TextColored(in UiThemeUtils.DimTextColor, "No unlock quest data available."); } private void DrawDutyStatus(bool? status, float statusIconSpacing) { float num = ImGui.GetColumnWidth() / 2f - statusIconSpacing; ImGui.SetCursorPosX(ImGui.GetCursorPosX() + num); if (!status.HasValue) { _uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusUnobtainable, FontAwesomeIcon.Minus); } else if (status.Value) { _uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusComplete, FontAwesomeIcon.Check); } else { _uiUtils.ChecklistItem(string.Empty, UiThemeUtils.StatusLocked, FontAwesomeIcon.Times); } } private void RebuildFlatRows() { List list = new List(); foreach (DutyCategory filteredCategory in _filteredCategories) { DutyCounts valueOrDefault = _categoryCounts.GetValueOrDefault(filteredCategory.Name, new DutyCounts()); if (valueOrDefault.Total == 0) { continue; } list.Add(new FlatRow(RowType.Category, 0, null, filteredCategory.Name, valueOrDefault)); if (!_expandedGroups.Contains(filteredCategory.Name)) { continue; } foreach (DutyInfo duty in filteredCategory.Duties) { list.Add(new FlatRow(RowType.Duty, 0, duty, string.Empty, new DutyCounts())); } } _flatRows = list; } public void UpdateFilter() { List list = new List(); AddCategory(list, "Dungeons", _dutyUnlockData.GetDungeons()); AddCategory(list, "Hard Dungeons", _dutyUnlockData.GetHardDungeons()); AddCategory(list, "Guildhests", _dutyUnlockData.GetGuildhests()); AddCategory(list, "Trials", _dutyUnlockData.GetTrials()); AddCategory(list, "Hard Trials", _dutyUnlockData.GetHardTrials()); AddCategory(list, "Extreme Trials", _dutyUnlockData.GetExtremeTrials()); AddCategory(list, "Unreal Trials", _dutyUnlockData.GetUnrealTrials()); AddCategory(list, "Normal Raids", _dutyUnlockData.GetNormalRaids()); AddCategory(list, "Savage Raids", _dutyUnlockData.GetSavageRaids()); AddCategory(list, "Alliance Raids", _dutyUnlockData.GetAllianceRaids()); AddCategory(list, "Chaotic Alliance Raids", _dutyUnlockData.GetChaoticAllianceRaids()); AddCategory(list, "Ultimate Raids", _dutyUnlockData.GetUltimateRaids()); AddCategory(list, "Deep Dungeons", _dutyUnlockData.GetDeepDungeons()); AddCategory(list, "Variant Dungeons", _dutyUnlockData.GetVariantDungeons()); AddCategory(list, "Criterion Dungeons", _dutyUnlockData.GetCriterionDungeons()); AddCategory(list, "Criterion Savage", _dutyUnlockData.GetCriterionSavageDungeons()); _filteredCategories = list.Where((DutyCategory c) => c.Duties.Count > 0).ToList(); RefreshCounts(); RebuildFlatRows(); } private void AddCategory(List categories, string name, IEnumerable duties) { List list = (from d in duties where MatchesFilter(d) orderby d.Level, d.ItemLevel, d.ContentFinderConditionId select d).ToList(); if (list.Count > 0) { categories.Add(new DutyCategory(name, list)); } } private bool MatchesFilter(DutyInfo duty) { if (!string.IsNullOrEmpty(_searchText) && !duty.Name.Contains(_searchText, StringComparison.OrdinalIgnoreCase)) { return false; } if (_showLockedOnly && duty.IsUnlocked) { return false; } if (_showUnlockedOnly && !duty.IsUnlocked) { return false; } return true; } public void RefreshCounts() { _categoryCounts.Clear(); foreach (DutyCategory filteredCategory in _filteredCategories) { int unlocked = filteredCategory.Duties.Count((DutyInfo d) => d.IsUnlocked); int completed = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted == true); int completionTrackable = filteredCategory.Duties.Count((DutyInfo d) => d.IsCompleted.HasValue); int count = filteredCategory.Duties.Count; _categoryCounts[filteredCategory.Name] = new DutyCounts(unlocked, completed, completionTrackable, count); } RebuildFlatRows(); } public void ClearCounts(int type, int code) { foreach (string item in _categoryCounts.Keys.ToList()) { _categoryCounts[item] = new DutyCounts(0, 0, 0, _categoryCounts[item].Total); } RebuildFlatRows(); } }