using System; using System.Collections.Generic; using System.Linq; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Lumina.Excel; using Lumina.Excel.Sheets; using QuestionableCompanion.Data; namespace QuestionableCompanion.Services; public class MSQProgressionService { private readonly IDataManager dataManager; private readonly IPluginLog log; private readonly QuestDetectionService questDetectionService; private readonly IClientState clientState; private readonly IFramework framework; private List? mainScenarioQuests; private Dictionary questNameCache = new Dictionary(); private Dictionary> questsByExpansion = new Dictionary>(); private static readonly uint[] MSQ_JOURNAL_GENRE_IDS = new uint[14] { 1u, 2u, 3u, 4u, 5u, 6u, 7u, 8u, 9u, 10u, 11u, 12u, 13u, 14u }; private const uint LAST_ARR_QUEST_ID = 65964u; private static readonly Dictionary JournalGenreToExpansion = new Dictionary { { 1u, MSQExpansionData.Expansion.ARealmReborn }, { 2u, MSQExpansionData.Expansion.ARealmReborn }, { 3u, MSQExpansionData.Expansion.Heavensward }, { 4u, MSQExpansionData.Expansion.Heavensward }, { 5u, MSQExpansionData.Expansion.Heavensward }, { 6u, MSQExpansionData.Expansion.Stormblood }, { 7u, MSQExpansionData.Expansion.Stormblood }, { 8u, MSQExpansionData.Expansion.Shadowbringers }, { 9u, MSQExpansionData.Expansion.Shadowbringers }, { 10u, MSQExpansionData.Expansion.Shadowbringers }, { 11u, MSQExpansionData.Expansion.Endwalker }, { 12u, MSQExpansionData.Expansion.Endwalker }, { 13u, MSQExpansionData.Expansion.Dawntrail }, { 14u, MSQExpansionData.Expansion.Dawntrail } }; public MSQProgressionService(IDataManager dataManager, IPluginLog log, QuestDetectionService questDetectionService, IClientState clientState, IFramework framework) { this.dataManager = dataManager; this.log = log; this.questDetectionService = questDetectionService; this.clientState = clientState; this.framework = framework; InitializeMSQData(); framework.RunOnTick(delegate { DebugCurrentCharacterQuest(); }, default(TimeSpan), 60); } private void InitializeMSQData() { try { log.Information("[MSQProgression] === INITIALIZING MSQ DATA ==="); ExcelSheet questSheet = dataManager.GetExcelSheet(); if (questSheet == null) { log.Error("[MSQProgression] Failed to load Quest sheet from Lumina!"); return; } int totalQuests = questSheet.Count(); log.Information($"[MSQProgression] ✓ Lumina Quest Sheet loaded: {totalQuests} total quests"); int manualCount = 0; foreach (Quest item in questSheet) { _ = item; manualCount++; } log.Information($"[MSQProgression] Manual iteration count: {manualCount} quests"); List highIdQuests = questSheet.Where((Quest q) => q.RowId > 66000).ToList(); log.Information($"[MSQProgression] Quests with RowId > 66000: {highIdQuests.Count}"); if (highIdQuests.Count > 0) { Quest firstHighId = highIdQuests.First(); log.Information($"[MSQProgression] First High ID Quest: {firstHighId.RowId}"); log.Information($"[MSQProgression] - Name: {firstHighId.Name}"); log.Information($"[MSQProgression] - Expansion.RowId: {firstHighId.Expansion.RowId}"); log.Information($"[MSQProgression] - JournalGenre.RowId: {firstHighId.JournalGenre.RowId}"); } log.Information("[MSQProgression] Analyzing JournalGenre distribution..."); foreach (IGrouping group in (from q in questSheet where q.RowId != 0 group q by q.JournalGenre.RowId into g orderby g.Key select g).Take(10)) { log.Information($"[MSQProgression] Genre {group.Key}: {group.Count()} quests"); } log.Information("[MSQProgression] Filtering MSQ quests by JournalGenre categories (1-14)..."); mainScenarioQuests = (from q in questSheet where ((ReadOnlySpan)MSQ_JOURNAL_GENRE_IDS).Contains(q.JournalGenre.RowId) orderby q.RowId select q).ToList(); log.Information($"[MSQProgression] ✓ Found {mainScenarioQuests.Count} total MSQ quests across all expansions!"); if (mainScenarioQuests.Count == 0) { log.Error("[MSQProgression] No MSQ quests found! JournalGenre filter may be incorrect."); return; } log.Information("[MSQProgression] === DETAILED MSQ QUEST ANALYSIS (First 20) ==="); foreach (Quest quest in mainScenarioQuests.Take(20)) { log.Information($"[MSQProgression] Quest {quest.RowId}:"); log.Information($"[MSQProgression] - Name: {quest.Name}"); log.Information($"[MSQProgression] - Expansion.RowId: {quest.Expansion.RowId}"); try { string expansionName = quest.Expansion.Value.Name.ToString(); log.Information("[MSQProgression] - Expansion.Name: " + expansionName); } catch (Exception ex) { log.Information("[MSQProgression] - Expansion.Name: ERROR - " + ex.Message); } log.Information($"[MSQProgression] - JournalGenre.RowId: {quest.JournalGenre.RowId}"); } log.Information("[MSQProgression] === MSQ QUESTS BY JOURNALGENRE (EXPANSION) ==="); foreach (IGrouping group2 in from q in mainScenarioQuests group q by q.JournalGenre.RowId into g orderby g.Key select g) { uint genreId = group2.Key; MSQExpansionData.Expansion expansion = JournalGenreToExpansion.GetValueOrDefault(genreId, MSQExpansionData.Expansion.ARealmReborn); string genreName = group2.First().JournalGenre.Value.Name.ToString(); string sampleQuests = string.Join(", ", from q in group2.Take(3) select q.RowId); log.Information($"[MSQProgression] JournalGenre {genreId} ({expansion}):"); log.Information("[MSQProgression] - Name: " + genreName); log.Information($"[MSQProgression] - Count: {group2.Count()} quests"); log.Information("[MSQProgression] - Samples: " + sampleQuests + "..."); } log.Information("[MSQProgression] === MSQ QUESTS BY EXPANSION (GROUPED) ==="); foreach (IGrouping group3 in from q in mainScenarioQuests group q by JournalGenreToExpansion.GetValueOrDefault(q.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn) into g orderby g.Key select g) { log.Information($"[MSQProgression] {group3.Key}: {group3.Count()} quests total"); } MSQExpansionData.ClearQuests(); log.Information("[MSQProgression] Building expansion quest mappings..."); foreach (Quest quest2 in mainScenarioQuests) { string name = quest2.Name.ToString(); if (!string.IsNullOrEmpty(name)) { questNameCache[quest2.RowId] = name; } MSQExpansionData.Expansion expansion2 = JournalGenreToExpansion.GetValueOrDefault(quest2.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn); if (quest2.JournalGenre.RowId != 2 || quest2.RowId <= 65964) { MSQExpansionData.RegisterQuest(quest2.RowId, expansion2); string shortName = MSQExpansionData.GetExpansionShortName(expansion2); if (!questsByExpansion.ContainsKey(shortName)) { questsByExpansion[shortName] = new List(); } questsByExpansion[shortName].Add(quest2); } } log.Information("[MSQProgression] === EXPANSION BREAKDOWN ==="); foreach (MSQExpansionData.Expansion exp in MSQExpansionData.GetAllExpansions()) { string shortName2 = MSQExpansionData.GetExpansionShortName(exp); List quests = questsByExpansion.GetValueOrDefault(shortName2); int count = quests?.Count ?? 0; if (count > 0 && quests != null) { string sampleIds = string.Join(", ", from q in quests.Take(5) select q.RowId); log.Information($"[MSQProgression] ✓ {MSQExpansionData.GetExpansionName(exp)} ({shortName2}): {count} quests (IDs: {sampleIds}...)"); } else { log.Warning($"[MSQProgression] ⚠ {MSQExpansionData.GetExpansionName(exp)} ({shortName2}): {count} quests (EMPTY!)"); } } log.Information("[MSQProgression] === MSQ DATA INITIALIZATION COMPLETE ==="); } catch (Exception ex2) { log.Error("[MSQProgression] EXCEPTION during MSQ data initialization: " + ex2.Message); log.Error("[MSQProgression] Stack trace: " + ex2.StackTrace); } } public (uint questId, string questName) GetLastCompletedMSQ(string characterName) { if (mainScenarioQuests == null || mainScenarioQuests.Count == 0) { return (questId: 0u, questName: "—"); } try { List completedQuests = questDetectionService.GetAllCompletedQuestIds(); Quest lastMSQ = (from q in mainScenarioQuests where completedQuests.Contains(q.RowId) orderby q.RowId descending select q).FirstOrDefault(); if (lastMSQ.RowId != 0) { string questName = questNameCache.GetValueOrDefault(lastMSQ.RowId, "Unknown Quest"); return (questId: lastMSQ.RowId, questName: questName); } } catch (Exception ex) { log.Error("[MSQProgression] Failed to get last completed MSQ: " + ex.Message); } return (questId: 0u, questName: "—"); } public float GetMSQCompletionPercentage() { if (mainScenarioQuests == null || mainScenarioQuests.Count == 0) { return 0f; } try { List completedQuests = questDetectionService.GetAllCompletedQuestIds(); return (float)mainScenarioQuests.Count((Quest q) => completedQuests.Contains(q.RowId)) / (float)mainScenarioQuests.Count * 100f; } catch (Exception ex) { log.Error("[MSQProgression] Failed to calculate MSQ completion: " + ex.Message); return 0f; } } public int GetTotalMSQCount() { return mainScenarioQuests?.Count ?? 0; } public int GetCompletedMSQCount() { if (mainScenarioQuests == null || mainScenarioQuests.Count == 0) { return 0; } try { List completedQuests = questDetectionService.GetAllCompletedQuestIds(); return mainScenarioQuests.Count((Quest q) => completedQuests.Contains(q.RowId)); } catch (Exception ex) { log.Error("[MSQProgression] Failed to get completed MSQ count: " + ex.Message); return 0; } } public string GetQuestName(uint questId) { return questNameCache.GetValueOrDefault(questId, "Unknown Quest"); } public bool IsMSQ(uint questId) { return mainScenarioQuests?.Any((Quest q) => q.RowId == questId) ?? false; } public ExpansionInfo? GetExpansionForQuest(uint questId) { MSQExpansionData.Expansion expansion = MSQExpansionData.GetExpansionForQuest(questId); return new ExpansionInfo { Name = MSQExpansionData.GetExpansionName(expansion), ShortName = MSQExpansionData.GetExpansionShortName(expansion), MinQuestId = 0u, MaxQuestId = 0u, ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(expansion) }; } public List GetExpansions() { return (from exp in MSQExpansionData.GetAllExpansions() select new ExpansionInfo { Name = MSQExpansionData.GetExpansionName(exp), ShortName = MSQExpansionData.GetExpansionShortName(exp), MinQuestId = 0u, MaxQuestId = 0u, ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(exp) }).ToList(); } public (int completed, int total) GetExpansionProgress(string expansionShortName) { List completedQuests = questDetectionService.GetAllCompletedQuestIds(); List? obj = questsByExpansion.GetValueOrDefault(expansionShortName) ?? new List(); int completed = obj.Count((Quest q) => completedQuests.Contains(q.RowId)); int total = obj.Count; return (completed: completed, total: total); } public ExpansionInfo? GetCurrentExpansion() { try { log.Information("[MSQProgression] ========================================"); log.Information("[MSQProgression] === DETECTING CURRENT EXPANSION ==="); log.Information("[MSQProgression] ========================================"); List completedQuests = questDetectionService.GetAllCompletedQuestIds(); log.Information($"[MSQProgression] Total completed quests: {completedQuests.Count}"); log.Information("[MSQProgression] METHOD 1: Using AgentScenarioTree (Game Data)"); log.Information("[MSQProgression] ------------------------------------------------"); (MSQExpansionData.Expansion expansion, string debugInfo) currentExpansionFromGameWithDebug = MSQExpansionData.GetCurrentExpansionFromGameWithDebug(); MSQExpansionData.Expansion gameExpansion = currentExpansionFromGameWithDebug.expansion; string[] array = currentExpansionFromGameWithDebug.debugInfo.Split('\n'); foreach (string line in array) { if (!string.IsNullOrWhiteSpace(line)) { log.Information("[MSQProgression] " + line); } } log.Information("[MSQProgression] Game Data Result: " + MSQExpansionData.GetExpansionName(gameExpansion)); log.Information("[MSQProgression] METHOD 2: Using Completed Quests Analysis"); log.Information("[MSQProgression] ------------------------------------------------"); MSQExpansionData.Expansion analysisExpansion = MSQExpansionData.GetCurrentExpansion(completedQuests); log.Information("[MSQProgression] Analysis Result: " + MSQExpansionData.GetExpansionName(analysisExpansion)); array = MSQExpansionData.GetExpansionDetectionDebugInfo(completedQuests).Split('\n'); foreach (string line2 in array) { if (!string.IsNullOrWhiteSpace(line2)) { log.Debug("[MSQProgression] " + line2); } } log.Information("[MSQProgression] COMPARISON:"); log.Information("[MSQProgression] Game Data: " + MSQExpansionData.GetExpansionName(gameExpansion)); log.Information("[MSQProgression] Analysis: " + MSQExpansionData.GetExpansionName(analysisExpansion)); MSQExpansionData.Expansion finalExpansion = gameExpansion; if (gameExpansion == MSQExpansionData.Expansion.ARealmReborn && analysisExpansion != MSQExpansionData.Expansion.ARealmReborn) { log.Warning("[MSQProgression] Game data returned ARR but analysis found higher expansion!"); log.Warning("[MSQProgression] Using analysis result: " + MSQExpansionData.GetExpansionName(analysisExpansion)); finalExpansion = analysisExpansion; } log.Information("[MSQProgression] ========================================"); log.Information("[MSQProgression] >>> FINAL EXPANSION: " + MSQExpansionData.GetExpansionName(finalExpansion) + " <<<"); log.Information("[MSQProgression] ========================================"); return new ExpansionInfo { Name = MSQExpansionData.GetExpansionName(finalExpansion), ShortName = MSQExpansionData.GetExpansionShortName(finalExpansion), MinQuestId = 0u, MaxQuestId = 0u, ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(finalExpansion) }; } catch (Exception ex) { log.Error("[MSQProgression] Error detecting expansion: " + ex.Message); log.Error("[MSQProgression] Stack: " + ex.StackTrace); return GetExpansions().FirstOrDefault(); } } public Dictionary GetExpansionProgressForCharacter(List completedQuestIds) { Dictionary result = new Dictionary(); foreach (ExpansionInfo exp in GetExpansions()) { List expansionQuests = questsByExpansion.GetValueOrDefault(exp.ShortName) ?? new List(); int completed = expansionQuests.Count((Quest q) => completedQuestIds.Contains(q.RowId)); result[exp.ShortName] = (completed, expansionQuests.Count); } return result; } public List GetAllMSQQuests() { return mainScenarioQuests ?? new List(); } public Dictionary GetExpansionProgress() { Dictionary result = new Dictionary(); List completedQuests = questDetectionService.GetAllCompletedQuestIds(); foreach (MSQExpansionData.Expansion expansion in MSQExpansionData.GetAllExpansions()) { ExpansionProgress progress = MSQExpansionData.GetExpansionProgress(completedQuests, expansion); result[progress.ExpansionName] = new ExpansionProgressInfo { ExpansionName = progress.ExpansionName, ShortName = progress.ExpansionShortName, TotalQuests = progress.ExpectedCount, CompletedQuests = progress.CompletedCount, Percentage = progress.Percentage }; } return result; } public Dictionary GetExpansionProgressForCharacter(List completedQuestIds) { Dictionary result = new Dictionary(); uint result2; List completedQuestIdsUint = (from id in completedQuestIds select uint.TryParse(id, out result2) ? result2 : 0u into id where id != 0 select id).ToList(); foreach (MSQExpansionData.Expansion expansion in MSQExpansionData.GetAllExpansions()) { ExpansionProgress progress = MSQExpansionData.GetExpansionProgress(completedQuestIdsUint, expansion); result[progress.ExpansionName] = new ExpansionProgressInfo { ExpansionName = progress.ExpansionName, ShortName = progress.ExpansionShortName, TotalQuests = progress.ExpectedCount, CompletedQuests = progress.CompletedCount, Percentage = progress.Percentage }; } return result; } public ExpansionInfo? GetCurrentExpansion(uint lastCompletedQuestId) { MSQExpansionData.Expansion expansion = MSQExpansionData.GetExpansionForQuest(lastCompletedQuestId); return new ExpansionInfo { Name = MSQExpansionData.GetExpansionName(expansion), ShortName = MSQExpansionData.GetExpansionShortName(expansion), MinQuestId = 0u, MaxQuestId = 0u, ExpectedQuestCount = MSQExpansionData.GetExpectedQuestCount(expansion) }; } private MSQExpansionData.Expansion ConvertLuminaExpansionToOurs(uint luminaExpansionId) { return luminaExpansionId switch { 0u => MSQExpansionData.Expansion.ARealmReborn, 1u => MSQExpansionData.Expansion.Heavensward, 2u => MSQExpansionData.Expansion.Stormblood, 3u => MSQExpansionData.Expansion.Shadowbringers, 4u => MSQExpansionData.Expansion.Endwalker, 5u => MSQExpansionData.Expansion.Dawntrail, _ => MSQExpansionData.Expansion.ARealmReborn, }; } public void DebugCurrentCharacterQuest() { try { log.Information("[MSQProgression] === DEBUG CURRENT CHARACTER QUEST ==="); IPlayerCharacter player = clientState.LocalPlayer; if (player == null) { log.Warning("[MSQProgression] LocalPlayer is null - not logged in yet?"); framework.RunOnTick(delegate { DebugCurrentCharacterQuest(); }, default(TimeSpan), 60); return; } string characterName = player.Name.TextValue; string worldName = player.HomeWorld.Value.Name.ToString(); log.Information("[MSQProgression] Character: " + characterName + " @ " + worldName); ExcelSheet questSheet = dataManager.GetExcelSheet(); if (questSheet == null) { log.Error("[MSQProgression] Failed to load Quest sheet!"); return; } List completedMSQQuests = new List(); log.Information("[MSQProgression] Checking MSQ quest completion..."); foreach (Quest quest in questSheet) { if (((ReadOnlySpan)MSQ_JOURNAL_GENRE_IDS).Contains(quest.JournalGenre.RowId) && QuestManager.IsQuestComplete((ushort)quest.RowId)) { completedMSQQuests.Add(quest); } } log.Information($"[MSQProgression] Character has {completedMSQQuests.Count} completed MSQ quests"); if (completedMSQQuests.Count == 0) { log.Warning("[MSQProgression] No completed MSQ quests found!"); return; } Quest latestMSQQuest = completedMSQQuests.OrderByDescending((Quest quest2) => quest2.RowId).First(); log.Information($"[MSQProgression] Latest completed MSQ quest ID: {latestMSQQuest.RowId}"); Quest questData = latestMSQQuest; log.Information("[MSQProgression] === LATEST MSQ QUEST DETAILS ==="); log.Information($"[MSQProgression] Quest ID: {questData.RowId}"); log.Information($"[MSQProgression] Quest Name: {questData.Name}"); log.Information($"[MSQProgression] JournalGenre.RowId: {questData.JournalGenre.RowId}"); try { string genreName = questData.JournalGenre.Value.Name.ToString(); log.Information("[MSQProgression] JournalGenre.Name: " + genreName); } catch { log.Information("[MSQProgression] JournalGenre.Name: ERROR"); } log.Information($"[MSQProgression] Expansion.RowId: {questData.Expansion.RowId}"); try { string expansionName = questData.Expansion.Value.Name.ToString(); log.Information("[MSQProgression] Expansion.Name: " + expansionName); } catch { log.Information("[MSQProgression] Expansion.Name: ERROR"); } log.Information("[MSQProgression] === CHARACTER IS IN THIS EXPANSION ==="); log.Information($"[MSQProgression] Character is at Quest {questData.RowId} which is in:"); log.Information($"[MSQProgression] - JournalGenre: {questData.JournalGenre.RowId}"); log.Information($"[MSQProgression] - Expansion: {questData.Expansion.RowId}"); log.Information("[MSQProgression] === RECENT COMPLETED MSQ QUESTS (Last 10) ==="); foreach (Quest q in completedMSQQuests.OrderByDescending((Quest quest2) => quest2.RowId).Take(10).ToList()) { log.Information($"[MSQProgression] Quest {q.RowId}: {q.Name} (Genre: {q.JournalGenre.RowId}, Exp: {q.Expansion.RowId})"); } log.Information("[MSQProgression] === COMPLETED MSQ QUESTS BY EXPANSION ==="); foreach (IGrouping group in from quest2 in completedMSQQuests group quest2 by JournalGenreToExpansion.GetValueOrDefault(quest2.JournalGenre.RowId, MSQExpansionData.Expansion.ARealmReborn) into g orderby g.Key select g) { log.Information($"[MSQProgression] {group.Key}: {group.Count()} quests completed"); } } catch (Exception ex) { log.Error("[MSQProgression] ERROR in DebugCurrentCharacterQuest: " + ex.Message); log.Error("[MSQProgression] Stack trace: " + ex.StackTrace); } } }