Files
MSQProgress/ProgressWindow.cs
2026-02-21 18:57:38 -05:00

166 lines
6.5 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.12.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<ContentFinderCondition>();
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.");
}
/// <summary>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.</summary>
private (int until, int fromStart) GetQuestsUntilNextInstanceUnlock(IEnumerable<ContentFinderCondition> 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<ContentFinderCondition> GetDutiesForDisplay(IEnumerable<ContentFinderCondition> 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",
_ => "?"
};
}
}