qstbak/QuestionableCompanion/QuestionableCompanion.Services/MSQProgressionService.cs
2025-12-04 04:39:08 +10:00

602 lines
22 KiB
C#

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<Quest>? mainScenarioQuests;
private Dictionary<uint, string> questNameCache = new Dictionary<uint, string>();
private Dictionary<string, List<Quest>> questsByExpansion = new Dictionary<string, List<Quest>>();
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<uint, MSQExpansionData.Expansion> JournalGenreToExpansion = new Dictionary<uint, MSQExpansionData.Expansion>
{
{
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<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
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<Quest> 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<uint, Quest> 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<uint>)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<uint, Quest> 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<MSQExpansionData.Expansion, Quest> 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<Quest>();
}
questsByExpansion[shortName].Add(quest2);
}
}
log.Information("[MSQProgression] === EXPANSION BREAKDOWN ===");
foreach (MSQExpansionData.Expansion exp in MSQExpansionData.GetAllExpansions())
{
string shortName2 = MSQExpansionData.GetExpansionShortName(exp);
List<Quest> 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<uint> 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<uint> 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<uint> 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<ExpansionInfo> 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<uint> completedQuests = questDetectionService.GetAllCompletedQuestIds();
List<Quest>? obj = questsByExpansion.GetValueOrDefault(expansionShortName) ?? new List<Quest>();
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<uint> 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<string, (int completed, int total)> GetExpansionProgressForCharacter(List<uint> completedQuestIds)
{
Dictionary<string, (int, int)> result = new Dictionary<string, (int, int)>();
foreach (ExpansionInfo exp in GetExpansions())
{
List<Quest> expansionQuests = questsByExpansion.GetValueOrDefault(exp.ShortName) ?? new List<Quest>();
int completed = expansionQuests.Count((Quest q) => completedQuestIds.Contains(q.RowId));
result[exp.ShortName] = (completed, expansionQuests.Count);
}
return result;
}
public List<Quest> GetAllMSQQuests()
{
return mainScenarioQuests ?? new List<Quest>();
}
public Dictionary<string, ExpansionProgressInfo> GetExpansionProgress()
{
Dictionary<string, ExpansionProgressInfo> result = new Dictionary<string, ExpansionProgressInfo>();
List<uint> 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<string, ExpansionProgressInfo> GetExpansionProgressForCharacter(List<string> completedQuestIds)
{
Dictionary<string, ExpansionProgressInfo> result = new Dictionary<string, ExpansionProgressInfo>();
uint result2;
List<uint> 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<Quest> questSheet = dataManager.GetExcelSheet<Quest>();
if (questSheet == null)
{
log.Error("[MSQProgression] Failed to load Quest sheet!");
return;
}
List<Quest> completedMSQQuests = new List<Quest>();
log.Information("[MSQProgression] Checking MSQ quest completion...");
foreach (Quest quest in questSheet)
{
if (((ReadOnlySpan<uint>)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<MSQExpansionData.Expansion, Quest> 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);
}
}
}