using System; using System.Collections.Generic; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Application.Network.WorkDefinitions; using FFXIVClientStructs.FFXIV.Client.Game; namespace QuestionableCompanion.Services; public class QuestDetectionService : IDisposable { private readonly IFramework framework; private readonly IPluginLog log; private readonly IClientState clientState; private readonly HashSet acceptedQuests = new HashSet(); private readonly HashSet completedQuests = new HashSet(); private HashSet completedQuestCache = new HashSet(); private DateTime lastCacheRefresh = DateTime.MinValue; private const int CACHE_REFRESH_MINUTES = 5; public event Action? QuestAccepted; public event Action? QuestCompleted; public QuestDetectionService(IFramework framework, IPluginLog log, IClientState clientState) { this.framework = framework; this.log = log; this.clientState = clientState; framework.Update += OnFrameworkUpdate; log.Information("[QuestDetection] Service initialized"); } private void OnFrameworkUpdate(IFramework framework) { if (!clientState.IsLoggedIn) { return; } try { CheckQuestUpdates(); } catch (Exception ex) { log.Debug("[QuestDetection] Error in framework update: " + ex.Message); } } private unsafe void CheckQuestUpdates() { QuestManager* questManager = QuestManager.Instance(); if (questManager == null) { log.Debug("[QuestDetection] QuestManager instance is null"); return; } try { Span normalQuests = questManager->NormalQuests; if (normalQuests.Length == 0) { log.Debug("[QuestDetection] NormalQuests array is empty"); return; } int maxSlots = Math.Min(normalQuests.Length, 30); for (int i = 0; i < maxSlots; i++) { try { QuestWork quest = normalQuests[i]; if (quest.QuestId == 0) { continue; } uint questId = quest.QuestId; if (!acceptedQuests.Contains(questId)) { if (!IsQuestComplete(questId)) { acceptedQuests.Add(questId); string questName = GetQuestName(questId); log.Information($"[QuestDetection] Quest Accepted: {questId} - {questName}"); this.QuestAccepted?.Invoke(questId, questName); } } else if (!completedQuests.Contains(questId) && IsQuestComplete(questId)) { completedQuests.Add(questId); string questName2 = GetQuestName(questId); log.Information($"[QuestDetection] Quest Completed: {questId} - {questName2}"); this.QuestCompleted?.Invoke(questId, questName2); } } catch (IndexOutOfRangeException) { log.Debug($"[QuestDetection] Index {i} out of range, stopping quest check"); break; } catch (Exception ex2) { log.Debug($"[QuestDetection] Error checking quest slot {i}: {ex2.Message}"); } } } catch (Exception ex3) { log.Warning("[QuestDetection] Error accessing quest data: " + ex3.Message); } } private bool IsQuestComplete(uint questId) { try { return QuestManager.IsQuestComplete(questId); } catch { return false; } } public unsafe bool IsQuestCompletedDirect(uint questId) { try { if (QuestManager.Instance() == null) { log.Warning("[QuestDetection] QuestManager instance not available"); return false; } bool isComplete = QuestManager.IsQuestComplete(questId); log.Debug($"[QuestDetection] Quest {questId} completion status: {isComplete}"); return isComplete; } catch (Exception ex) { log.Error($"[QuestDetection] Failed to check quest {questId}: {ex.Message}"); return false; } } public unsafe List GetAllCompletedQuestIds() { List completed = new List(); try { if (QuestManager.Instance() == null) { log.Warning("[QuestDetection] QuestManager instance not available"); return completed; } log.Information("[QuestDetection] Scanning for completed quests..."); foreach (var item in new List<(uint, uint)> { (1u, 3000u), (65000u, 71000u) }) { uint start = item.Item1; uint end = item.Item2; for (uint i = start; i <= end; i++) { try { if (QuestManager.IsQuestComplete(i)) { completed.Add(i); } } catch { } } } log.Information($"[QuestDetection] Retrieved {completed.Count} completed quests"); } catch (Exception ex) { log.Error("[QuestDetection] Error while fetching completed quests: " + ex.Message); } return completed; } public void RefreshQuestCache() { try { log.Information("[QuestDetection] Refreshing quest cache..."); List allCompleted = GetAllCompletedQuestIds(); completedQuestCache = new HashSet(allCompleted); lastCacheRefresh = DateTime.Now; log.Information($"[QuestDetection] Quest cache refreshed with {completedQuestCache.Count} completed quests"); } catch (Exception ex) { log.Error("[QuestDetection] Failed to refresh quest cache: " + ex.Message); } } public bool IsQuestCompletedCached(uint questId) { if (completedQuestCache.Count == 0 || (DateTime.Now - lastCacheRefresh).TotalMinutes > 5.0) { RefreshQuestCache(); } return completedQuestCache.Contains(questId); } private string GetQuestName(uint questId) { try { return $"Quest {questId}"; } catch { return $"Quest {questId}"; } } public void ResetTracking() { acceptedQuests.Clear(); completedQuests.Clear(); completedQuestCache.Clear(); lastCacheRefresh = DateTime.MinValue; log.Information("[QuestDetection] Tracking reset"); } public bool IsQuestAccepted(uint questId) { return acceptedQuests.Contains(questId); } public bool IsQuestCompleted(uint questId) { return completedQuests.Contains(questId); } public void Dispose() { framework.Update -= OnFrameworkUpdate; acceptedQuests.Clear(); completedQuests.Clear(); completedQuestCache.Clear(); log.Information("[QuestDetection] Service disposed"); } }