using System; using System.Collections.Generic; using System.IO; using System.Linq; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Newtonsoft.Json; namespace QuestionableCompanion.Services; public class QuestPreCheckService : IDisposable { private readonly IPluginLog log; private readonly IClientState clientState; private readonly Configuration config; private readonly AutoRetainerIPC autoRetainerIPC; private Dictionary preCheckResults = new Dictionary(); private Dictionary> questDatabase = new Dictionary>(); private Dictionary lastRefreshByCharacter = new Dictionary(); private readonly TimeSpan refreshInterval = TimeSpan.FromMinutes(30L); private string QuestDatabasePath { get { global::_003C_003Ey__InlineArray5 buffer = default(global::_003C_003Ey__InlineArray5); global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef, string>(ref buffer, 0) = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef, string>(ref buffer, 1) = "XIVLauncher"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef, string>(ref buffer, 2) = "pluginConfigs"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef, string>(ref buffer, 3) = "QuestionableCompanion"; global::_003CPrivateImplementationDetails_003E.InlineArrayElementRef, string>(ref buffer, 4) = "QuestDatabase.json"; return Path.Combine(global::_003CPrivateImplementationDetails_003E.InlineArrayAsReadOnlySpan, string>(in buffer, 5)); } } public QuestPreCheckService(IPluginLog log, IClientState clientState, Configuration config, AutoRetainerIPC autoRetainerIPC) { this.log = log; this.clientState = clientState; this.config = config; this.autoRetainerIPC = autoRetainerIPC; LoadQuestDatabase(); } private void LoadQuestDatabase() { try { EnsureQuestDatabasePath(); if (!File.Exists(QuestDatabasePath)) { log.Information("[QuestPreCheck] Creating new quest database..."); questDatabase = new Dictionary>(); return; } string json = File.ReadAllText(QuestDatabasePath); if (string.IsNullOrEmpty(json)) { questDatabase = new Dictionary>(); return; } questDatabase = JsonConvert.DeserializeObject>>(json) ?? new Dictionary>(); log.Information($"[QuestPreCheck] Loaded quest database for {questDatabase.Count} characters"); } catch (Exception ex) { log.Error("[QuestPreCheck] Error loading quest database: " + ex.Message); questDatabase = new Dictionary>(); } } private void SaveQuestDatabase() { try { EnsureQuestDatabasePath(); string json = JsonConvert.SerializeObject(questDatabase, Formatting.Indented); File.WriteAllText(QuestDatabasePath, json); log.Information($"[QuestPreCheck] Quest database saved ({questDatabase.Count} characters)"); } catch (Exception ex) { log.Error("[QuestPreCheck] Error saving quest database: " + ex.Message); } } private void EnsureQuestDatabasePath() { string directory = Path.GetDirectoryName(QuestDatabasePath); if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } } public unsafe void ScanCurrentCharacterQuestStatus(bool verbose = false) { if (clientState.LocalPlayer == null) { log.Warning("[QuestPreCheck] No local player found"); return; } string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString(); string charName = $"{clientState.LocalPlayer.Name}@{worldName}"; if (verbose) { log.Information("[QuestPreCheck] Scanning quest status for: " + charName); } if (!questDatabase.ContainsKey(charName)) { questDatabase[charName] = new Dictionary(); } if (QuestManager.Instance() == null) { log.Error("[QuestPreCheck] QuestManager not available"); return; } int questsScanned = 0; int questsCompleted = 0; int questsChanged = 0; List newlyCompleted = new List(); List questsToScan = config.QuestPreCheckRange ?? new List(); if (questsToScan.Count == 0) { for (uint questId = 1u; questId <= 4500; questId++) { questsToScan.Add(questId); } } foreach (uint questId2 in questsToScan) { try { bool num = QuestManager.IsQuestComplete((ushort)(questId2 % 65536)); questsScanned++; if (num) { questsCompleted++; if (!questDatabase[charName].GetValueOrDefault(questId2, defaultValue: false)) { questDatabase[charName][questId2] = true; questsChanged++; newlyCompleted.Add(questId2); if (verbose) { log.Debug($"[QuestPreCheck] {charName} - Quest {questId2}: ✓ NEWLY COMPLETED"); } } else { questDatabase[charName][questId2] = true; } } if (verbose && questId2 % 500 == 0) { log.Debug($"[QuestPreCheck] Progress: {questId2}/{questsToScan.Count} quests scanned..."); } } catch (Exception ex) { log.Error($"[QuestPreCheck] Error checking quest {questId2}: {ex.Message}"); } } if (verbose) { log.Information($"[QuestPreCheck] Scan complete: {questsScanned} checked, {questsCompleted} completed, {questsChanged} changed"); if (newlyCompleted.Count > 0) { log.Information("[QuestPreCheck] NEWLY COMPLETED: " + string.Join(", ", newlyCompleted)); } } lastRefreshByCharacter[charName] = DateTime.Now; SaveQuestDatabase(); } public void RefreshQuestDatabasePeriodic() { if (clientState.LocalPlayer != null && clientState.IsLoggedIn) { string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString(); string charName = $"{clientState.LocalPlayer.Name}@{worldName}"; if (!lastRefreshByCharacter.TryGetValue(charName, out var lastRefresh) || DateTime.Now - lastRefresh >= refreshInterval) { log.Information("[QuestDB] === 30-MINUTE REFRESH TRIGGERED ==="); log.Information("[QuestDB] Updating quest status for: " + charName); ScanCurrentCharacterQuestStatus(verbose: true); log.Information("[QuestDB] === 30-MINUTE REFRESH COMPLETE ==="); } } } public void LogCompletedQuestsBeforeLogout() { if (clientState.LocalPlayer != null) { string worldName = clientState.LocalPlayer.HomeWorld.Value.Name.ToString(); string charName = $"{clientState.LocalPlayer.Name}@{worldName}"; log.Information("[QuestDB] Logging final quest status before logout: " + charName); ScanCurrentCharacterQuestStatus(); log.Information("[QuestDB] Final quest state saved for: " + charName); } } public Dictionary PerformPreRotationCheck(uint stopQuestId, List characters) { log.Information("[QuestPreCheck] === STARTING PRE-ROTATION QUEST VERIFICATION ==="); log.Information($"[QuestPreCheck] Checking {characters.Count} characters for quest {stopQuestId}..."); preCheckResults.Clear(); foreach (string character in characters) { try { if (questDatabase.ContainsKey(character) && questDatabase[character].ContainsKey(stopQuestId)) { bool isCompleted = questDatabase[character][stopQuestId]; preCheckResults[character] = isCompleted; string status = (isCompleted ? "✓ COMPLETED" : "○ PENDING"); log.Information($"[QuestPreCheck] {character}: {status} (from database)"); } else { log.Debug("[QuestPreCheck] " + character + ": Not in database, will check during rotation"); preCheckResults[character] = false; } } catch (Exception ex) { log.Error("[QuestPreCheck] Error checking " + character + ": " + ex.Message); preCheckResults[character] = false; } } log.Information("[QuestPreCheck] === PRE-ROTATION CHECK COMPLETE ==="); return preCheckResults; } public bool ShouldSkipCharacter(string characterName, uint questId) { if (preCheckResults.TryGetValue(characterName, out var isCompleted) && isCompleted) { log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} - SKIPPING"); return true; } bool completed = default(bool); if (questDatabase.TryGetValue(characterName, out Dictionary quests) && quests.TryGetValue(questId, out completed) && completed) { log.Information($"[QuestPreCheck] Character {characterName} already completed quest {questId} (from DB) - SKIPPING"); return true; } return false; } public bool? GetQuestStatus(string characterName, uint questId) { if (questDatabase.TryGetValue(characterName, out Dictionary quests) && quests.TryGetValue(questId, out var isCompleted)) { return isCompleted; } return null; } public List GetCompletedQuests(string characterName) { if (!questDatabase.TryGetValue(characterName, out Dictionary quests)) { return new List(); } return (from kvp in quests where kvp.Value select kvp.Key).ToList(); } public void MarkQuestCompleted(string characterName, uint questId) { if (!questDatabase.ContainsKey(characterName)) { questDatabase[characterName] = new Dictionary(); } questDatabase[characterName][questId] = true; SaveQuestDatabase(); log.Information($"[QuestPreCheck] Marked quest {questId} as completed for {characterName}"); } public void ClearPreCheckResults() { preCheckResults.Clear(); log.Information("[QuestPreCheck] Pre-check results cleared"); } public void ClearCharacterData(string characterName) { if (questDatabase.ContainsKey(characterName)) { int questCount = questDatabase[characterName].Count; questDatabase.Remove(characterName); SaveQuestDatabase(); log.Information($"[QuestPreCheck] Cleared {questCount} quests for {characterName}"); } else { log.Information("[QuestPreCheck] No quest data found for " + characterName); } lastRefreshByCharacter.Remove(characterName); } public void Dispose() { SaveQuestDatabase(); log.Information("[QuestPreCheck] Service disposed"); } }