using System; using System.Collections.Generic; using System.Linq; using System.Text; using FFXIVClientStructs.FFXIV.Client.UI.Agent; namespace QuestionableCompanion.Data; public static class MSQExpansionData { public enum Expansion { ARealmReborn, Heavensward, Stormblood, Shadowbringers, Endwalker, Dawntrail } private static readonly Dictionary> ExpansionQuests = new Dictionary> { { Expansion.ARealmReborn, new HashSet() }, { Expansion.Heavensward, new HashSet() }, { Expansion.Stormblood, new HashSet() }, { Expansion.Shadowbringers, new HashSet() }, { Expansion.Endwalker, new HashSet() }, { Expansion.Dawntrail, new HashSet() } }; private static readonly Dictionary ExpectedQuestCounts = new Dictionary { { Expansion.ARealmReborn, 200 }, { Expansion.Heavensward, 100 }, { Expansion.Stormblood, 100 }, { Expansion.Shadowbringers, 100 }, { Expansion.Endwalker, 100 }, { Expansion.Dawntrail, 100 } }; private static readonly Dictionary ExpansionNames = new Dictionary { { Expansion.ARealmReborn, "A Realm Reborn" }, { Expansion.Heavensward, "Heavensward" }, { Expansion.Stormblood, "Stormblood" }, { Expansion.Shadowbringers, "Shadowbringers" }, { Expansion.Endwalker, "Endwalker" }, { Expansion.Dawntrail, "Dawntrail" } }; private static readonly Dictionary ExpansionShortNames = new Dictionary { { Expansion.ARealmReborn, "ARR" }, { Expansion.Heavensward, "HW" }, { Expansion.Stormblood, "SB" }, { Expansion.Shadowbringers, "ShB" }, { Expansion.Endwalker, "EW" }, { Expansion.Dawntrail, "DT" } }; public static void RegisterQuest(uint questId, Expansion expansion) { if (ExpansionQuests.TryGetValue(expansion, out HashSet quests)) { quests.Add(questId); } } public static void ClearQuests() { foreach (HashSet value in ExpansionQuests.Values) { value.Clear(); } } public static Expansion GetExpansionForQuest(uint questId) { foreach (var (expansion2, hashSet2) in ExpansionQuests) { if (hashSet2.Contains(questId)) { return expansion2; } } return Expansion.ARealmReborn; } public static IReadOnlySet GetQuestsForExpansion(Expansion expansion) { if (!ExpansionQuests.TryGetValue(expansion, out HashSet quests)) { return new HashSet(); } return quests; } public static int GetExpectedQuestCount(Expansion expansion) { if (!ExpectedQuestCounts.TryGetValue(expansion, out var count)) { return 0; } return count; } public static string GetExpansionName(Expansion expansion) { if (!ExpansionNames.TryGetValue(expansion, out string name)) { return "Unknown"; } return name; } public static string GetExpansionShortName(Expansion expansion) { if (!ExpansionShortNames.TryGetValue(expansion, out string name)) { return "???"; } return name; } public static IEnumerable GetAllExpansions() { return from e in Enum.GetValues() orderby (int)e select e; } public static int GetCompletedQuestCountForExpansion(IEnumerable completedQuestIds, Expansion expansion) { IReadOnlySet expansionQuests = GetQuestsForExpansion(expansion); return completedQuestIds.Count((uint qId) => expansionQuests.Contains(qId)); } public unsafe static (Expansion expansion, string debugInfo) GetCurrentExpansionFromGameWithDebug() { StringBuilder debug = new StringBuilder(); debug.AppendLine("=== AGENT SCENARIO TREE DEBUG ==="); try { AgentScenarioTree* agentScenarioTree = AgentScenarioTree.Instance(); StringBuilder stringBuilder = debug; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(30, 1, stringBuilder); handler.AppendLiteral("AgentScenarioTree.Instance(): "); handler.AppendFormatted((agentScenarioTree != null) ? "OK" : "NULL"); stringBuilder2.AppendLine(ref handler); if (agentScenarioTree == null) { debug.AppendLine("ERROR: AgentScenarioTree is NULL!"); return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString()); } stringBuilder = debug; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder); handler.AppendLiteral("AgentScenarioTree->Data: "); handler.AppendFormatted((agentScenarioTree->Data != null) ? "OK" : "NULL"); stringBuilder3.AppendLine(ref handler); if (agentScenarioTree->Data == null) { debug.AppendLine("ERROR: AgentScenarioTree->Data is NULL!"); return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString()); } ushort currentQuest = agentScenarioTree->Data->CurrentScenarioQuest; ushort completedQuest = agentScenarioTree->Data->CompleteScenarioQuest; stringBuilder = debug; StringBuilder stringBuilder4 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(33, 2, stringBuilder); handler.AppendLiteral("CurrentScenarioQuest (raw): "); handler.AppendFormatted(currentQuest); handler.AppendLiteral(" (0x"); handler.AppendFormatted(currentQuest, "X4"); handler.AppendLiteral(")"); stringBuilder4.AppendLine(ref handler); stringBuilder = debug; StringBuilder stringBuilder5 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(34, 2, stringBuilder); handler.AppendLiteral("CompleteScenarioQuest (raw): "); handler.AppendFormatted(completedQuest); handler.AppendLiteral(" (0x"); handler.AppendFormatted(completedQuest, "X4"); handler.AppendLiteral(")"); stringBuilder5.AppendLine(ref handler); ushort questToCheck = ((currentQuest != 0) ? currentQuest : completedQuest); stringBuilder = debug; StringBuilder stringBuilder6 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(25, 2, stringBuilder); handler.AppendLiteral("Quest to check: "); handler.AppendFormatted(questToCheck); handler.AppendLiteral(" (using "); handler.AppendFormatted((currentQuest != 0) ? "Current" : "Completed"); handler.AppendLiteral(")"); stringBuilder6.AppendLine(ref handler); if (questToCheck == 0) { debug.AppendLine("WARNING: Both CurrentScenarioQuest and CompleteScenarioQuest are 0!"); return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString()); } uint questId = (uint)(questToCheck | 0x10000); stringBuilder = debug; StringBuilder stringBuilder7 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(25, 2, stringBuilder); handler.AppendLiteral("Converted Quest ID: "); handler.AppendFormatted(questId); handler.AppendLiteral(" (0x"); handler.AppendFormatted(questId, "X8"); handler.AppendLiteral(")"); stringBuilder7.AppendLine(ref handler); Expansion expansion = GetExpansionForQuest(questId); stringBuilder = debug; StringBuilder stringBuilder8 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(22, 2, stringBuilder); handler.AppendLiteral("Expansion for Quest "); handler.AppendFormatted(questId); handler.AppendLiteral(": "); handler.AppendFormatted(GetExpansionName(expansion)); stringBuilder8.AppendLine(ref handler); IReadOnlySet expansionQuests = GetQuestsForExpansion(expansion); bool isRegistered = expansionQuests.Contains(questId); stringBuilder = debug; StringBuilder stringBuilder9 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(23, 3, stringBuilder); handler.AppendLiteral("Quest "); handler.AppendFormatted(questId); handler.AppendLiteral(" registered in "); handler.AppendFormatted(expansion); handler.AppendLiteral(": "); handler.AppendFormatted(isRegistered); stringBuilder9.AppendLine(ref handler); if (!isRegistered) { stringBuilder = debug; StringBuilder stringBuilder10 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(48, 1, stringBuilder); handler.AppendLiteral("WARNING: Quest "); handler.AppendFormatted(questId); handler.AppendLiteral(" is NOT in our registered quests!"); stringBuilder10.AppendLine(ref handler); stringBuilder = debug; StringBuilder stringBuilder11 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(30, 2, stringBuilder); handler.AppendLiteral("Total registered quests for "); handler.AppendFormatted(expansion); handler.AppendLiteral(": "); handler.AppendFormatted(expansionQuests.Count); stringBuilder11.AppendLine(ref handler); foreach (Expansion exp in GetAllExpansions()) { if (GetQuestsForExpansion(exp).Contains(questId)) { stringBuilder = debug; StringBuilder stringBuilder12 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(10, 1, stringBuilder); handler.AppendLiteral("FOUND in "); handler.AppendFormatted(exp); handler.AppendLiteral("!"); stringBuilder12.AppendLine(ref handler); expansion = exp; break; } } } stringBuilder = debug; StringBuilder stringBuilder13 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(25, 1, stringBuilder); handler.AppendLiteral(">>> FINAL EXPANSION: "); handler.AppendFormatted(GetExpansionName(expansion)); handler.AppendLiteral(" <<<"); stringBuilder13.AppendLine(ref handler); return (expansion: expansion, debugInfo: debug.ToString()); } catch (Exception ex) { StringBuilder stringBuilder = debug; StringBuilder stringBuilder14 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(11, 1, stringBuilder); handler.AppendLiteral("EXCEPTION: "); handler.AppendFormatted(ex.Message); stringBuilder14.AppendLine(ref handler); stringBuilder = debug; StringBuilder stringBuilder15 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(7, 1, stringBuilder); handler.AppendLiteral("Stack: "); handler.AppendFormatted(ex.StackTrace); stringBuilder15.AppendLine(ref handler); return (expansion: Expansion.ARealmReborn, debugInfo: debug.ToString()); } } public static Expansion GetCurrentExpansionFromGame() { return GetCurrentExpansionFromGameWithDebug().expansion; } public static Expansion GetCurrentExpansion(IEnumerable completedQuestIds) { List questList = completedQuestIds.ToList(); if (questList.Count == 0) { return Expansion.ARealmReborn; } foreach (Expansion expansion in GetAllExpansions().Reverse().ToList()) { IReadOnlySet expansionQuests = GetQuestsForExpansion(expansion); if (questList.Where((uint qId) => expansionQuests.Contains(qId)).ToList().Count > 0) { return expansion; } } return Expansion.ARealmReborn; } public static string GetExpansionDetectionDebugInfo(IEnumerable completedQuestIds) { List questList = completedQuestIds.ToList(); StringBuilder result = new StringBuilder(); result.AppendLine("=== EXPANSION DETECTION DEBUG ==="); StringBuilder stringBuilder = result; StringBuilder stringBuilder2 = stringBuilder; StringBuilder.AppendInterpolatedStringHandler handler = new StringBuilder.AppendInterpolatedStringHandler(24, 1, stringBuilder); handler.AppendLiteral("Total completed quests: "); handler.AppendFormatted(questList.Count); stringBuilder2.AppendLine(ref handler); result.AppendLine(""); result.AppendLine("Checking expansions from highest to lowest:"); result.AppendLine(""); foreach (Expansion expansion in GetAllExpansions().Reverse()) { IReadOnlySet expansionQuests = GetQuestsForExpansion(expansion); List completedInExpansion = questList.Where((uint qId) => expansionQuests.Contains(qId)).ToList(); float percentage = ((expansionQuests.Count > 0) ? ((float)completedInExpansion.Count / (float)expansionQuests.Count * 100f) : 0f); stringBuilder = result; StringBuilder stringBuilder3 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(4, 2, stringBuilder); handler.AppendFormatted(GetExpansionName(expansion)); handler.AppendLiteral(" ("); handler.AppendFormatted(GetExpansionShortName(expansion)); handler.AppendLiteral("):"); stringBuilder3.AppendLine(ref handler); stringBuilder = result; StringBuilder stringBuilder4 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(28, 1, stringBuilder); handler.AppendLiteral(" - Total MSQ in expansion: "); handler.AppendFormatted(expansionQuests.Count); stringBuilder4.AppendLine(ref handler); stringBuilder = result; StringBuilder stringBuilder5 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(32, 2, stringBuilder); handler.AppendLiteral(" - Completed by character: "); handler.AppendFormatted(completedInExpansion.Count); handler.AppendLiteral(" ("); handler.AppendFormatted(percentage, "F1"); handler.AppendLiteral("%)"); stringBuilder5.AppendLine(ref handler); if (completedInExpansion.Count > 0) { string samples = string.Join(", ", completedInExpansion.OrderByDescending((uint x) => x).Take(5)); stringBuilder = result; StringBuilder stringBuilder6 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(22, 1, stringBuilder); handler.AppendLiteral(" - Sample Quest IDs: "); handler.AppendFormatted(samples); stringBuilder6.AppendLine(ref handler); result.AppendLine(" >>> HAS COMPLETED QUESTS - WOULD SELECT THIS EXPANSION <<<"); } else { result.AppendLine(" - No quests completed in this expansion"); } result.AppendLine(""); } Expansion currentExpansion = GetCurrentExpansion(questList); result.AppendLine("==========================================="); stringBuilder = result; StringBuilder stringBuilder7 = stringBuilder; handler = new StringBuilder.AppendInterpolatedStringHandler(34, 1, stringBuilder); handler.AppendLiteral(">>> FINAL DETECTED EXPANSION: "); handler.AppendFormatted(GetExpansionName(currentExpansion)); handler.AppendLiteral(" <<<"); stringBuilder7.AppendLine(ref handler); result.AppendLine("==========================================="); return result.ToString(); } public static ExpansionProgress GetExpansionProgress(IEnumerable completedQuestIds, Expansion expansion) { int completed = GetCompletedQuestCountForExpansion(completedQuestIds, expansion); int expected = GetExpectedQuestCount(expansion); return new ExpansionProgress { Expansion = expansion, CompletedCount = completed, ExpectedCount = expected, Percentage = ((expected > 0) ? ((float)completed / (float)expected * 100f) : 0f), IsComplete = (completed >= expected) }; } public static List GetAllExpansionProgress(IEnumerable completedQuestIds) { return (from exp in GetAllExpansions() select GetExpansionProgress(completedQuestIds, exp)).ToList(); } }