using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Bindings.ImGui; using Dalamud.Interface.Windowing; using Lumina.Excel.Sheets; using CfcType = FFXIVClientStructs.FFXIV.Client.Game.InstanceContent.InstanceContentType; namespace MSQProgress; public class ProgressWindow : Window { private readonly MSQDataService _dataService; private int _drawCount; public ProgressWindow() : base("MSQ Progress", ImGuiWindowFlags.AlwaysAutoResize) { _dataService = new MSQDataService(Service.DataManager); } public override void Draw() { if (!Service.ClientState.IsLoggedIn) { ImGui.TextColored(new Vector4(1, 0.8f, 0.4f, 1), "Log in to see your MSQ progress."); return; } _drawCount++; if (_drawCount % 30 == 0) _dataService.Refresh(); if (ImGui.Button("Refresh")) _dataService.Refresh(); ImGui.SameLine(); ImGui.TextDisabled("(Progress updates automatically; use Refresh if needed)"); ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); ImGui.Text("Progress by expansion"); ImGui.TextDisabled("(Each expansion = base + all patch MSQ, e.g. ARR = 2.0 + 2.1–2.55)"); ImGui.Spacing(); var byExpansion = _dataService.ByExpansion; if (byExpansion.Count == 0) { ImGui.TextColored(new Vector4(0.8f, 0.6f, 0.2f, 1), "No MSQ data loaded. Try refreshing after opening the MSQ journal once."); return; } if (ImGui.BeginTable("msq_expansions", 4, ImGuiTableFlags.Borders | ImGuiTableFlags.RowBg)) { ImGui.TableSetupColumn("Expansion", ImGuiTableColumnFlags.WidthStretch); ImGui.TableSetupColumn("Completed", ImGuiTableColumnFlags.WidthFixed, 80); ImGui.TableSetupColumn("Total", ImGuiTableColumnFlags.WidthFixed, 60); ImGui.TableSetupColumn("Remaining", ImGuiTableColumnFlags.WidthFixed, 80); ImGui.TableHeadersRow(); foreach (var kv in byExpansion.OrderBy(x => x.Key)) { var prog = kv.Value; ImGui.TableNextRow(); ImGui.TableSetColumnIndex(0); var isCurrent = kv.Key == _dataService.CurrentExpansionRowId; if (isCurrent) ImGui.TextColored(new Vector4(0.4f, 0.9f, 0.5f, 1), $"{prog.Name} (current)"); else ImGui.Text(prog.Name); ImGui.TableSetColumnIndex(1); ImGui.Text(prog.CompletedQuests.ToString()); ImGui.TableSetColumnIndex(2); ImGui.Text(prog.TotalQuests.ToString()); ImGui.TableSetColumnIndex(3); ImGui.Text(prog.RemainingQuests.ToString()); } ImGui.EndTable(); } ImGui.Spacing(); ImGui.Separator(); ImGui.Spacing(); var cfcSheet = Service.DataManager.GetExcelSheet(); if (cfcSheet != null) { var (until, fromStart) = GetQuestsUntilNextInstanceUnlock(cfcSheet); var currentIndex = _dataService.CurrentGlobalIndex; if (until >= 0) ImGui.TextColored(new Vector4(0.6f, 0.85f, 1f, 1), $"{until} Quests remaining until next instance unlock"); else if (fromStart >= 0) ImGui.TextColored(new Vector4(0.6f, 0.85f, 1f, 1), $"At least {fromStart} quests until next instance unlock (open MSQ journal + Refresh for exact count)"); else if (currentIndex >= 0) ImGui.TextColored(new Vector4(0.7f, 0.7f, 0.6f, 1), "Next instance unlock unknown (open MSQ journal and Refresh)."); else ImGui.TextColored(new Vector4(0.5f, 0.7f, 0.5f, 1), "No instance unlocks remaining (or open MSQ journal and Refresh)."); } else ImGui.TextDisabled("Duty data unavailable."); } /// Returns (quests until next instance, from-start fallback). Uses MSQ order only (ignores game unlock state) so we always show the next instance in the story. private (int until, int fromStart) GetQuestsUntilNextInstanceUnlock(IEnumerable cfcSheet) { int minUntil = -1; int minFromStart = -1; var currentIndex = _dataService.CurrentGlobalIndex; foreach (var cfc in GetDutiesForDisplay(cfcSheet)) { if (!DutyUnlockMap.TryGetUnlockQuest(cfc.RowId, out var unlockQuestId, Service.DataManager)) unlockQuestId = DutyUnlockMap.GetUnlockQuestFromRow(cfc); if (unlockQuestId == 0) continue; var unlockStepIndex = _dataService.GetStepIndexForQuest(unlockQuestId); // Skip if we're past this unlock (we've completed that step) if (currentIndex >= 0 && unlockStepIndex >= 0 && unlockStepIndex < currentIndex) continue; int until; if (currentIndex >= 0 && unlockStepIndex >= 0 && unlockStepIndex == currentIndex) until = 0; // we're on the unlock quest right now else until = _dataService.QuestsUntilQuest(unlockQuestId); var fromStart = _dataService.QuestsFromStartToQuest(unlockQuestId); if (until >= 0 && (minUntil < 0 || until < minUntil)) minUntil = until; if (fromStart >= 0 && (minFromStart < 0 || fromStart < minFromStart)) minFromStart = fromStart; } return (minUntil, minFromStart); } private static IEnumerable GetDutiesForDisplay(IEnumerable sheet) { var dungeon = (uint)CfcType.Dungeon; var trial = (uint)CfcType.Trial; var raid = (uint)CfcType.Raid; foreach (var row in sheet) { if (!row.ContentType.IsValid) continue; var typeId = row.ContentType.RowId; if (typeId == dungeon || typeId == trial || typeId == raid) yield return row; } } private static string GetContentTypeLabel(ContentFinderCondition cfc) { if (!cfc.ContentType.IsValid) return "?"; var typeId = cfc.ContentType.RowId; return typeId switch { (uint)CfcType.Dungeon => "Dungeon", (uint)CfcType.Trial => "Trial", (uint)CfcType.Raid => "Raid", (uint)CfcType.GuildOrder => "Guildhest", _ => "?" }; } }