using System; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using Newtonsoft.Json.Linq; namespace QuestionableCompanion.Services; public class EventQuestExecutionService : IDisposable { private readonly AutoRetainerIPC autoRetainerIpc; private readonly QuestionableIPC questionableIPC; private readonly IPluginLog log; private readonly IFramework framework; private readonly ICommandManager commandManager; private readonly ICondition condition; private readonly Configuration configuration; private readonly EventQuestResolver eventQuestResolver; private EventQuestState currentState = new EventQuestState(); private Dictionary> eventQuestCompletionByCharacter = new Dictionary>(); private DateTime lastCheckTime = DateTime.MinValue; private const double CheckIntervalMs = 250.0; private bool isRotationActive; private string? lastTerritoryWaitDetected; private DateTime lastTerritoryTeleportTime = DateTime.MinValue; private Action? onDataChanged; public bool IsRotationActive => isRotationActive; public EventQuestExecutionService(AutoRetainerIPC autoRetainerIpc, QuestionableIPC questionableIPC, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, Configuration configuration, IDataManager dataManager, Action? onDataChanged = null) { this.autoRetainerIpc = autoRetainerIpc; this.questionableIPC = questionableIPC; this.log = log; this.framework = framework; this.commandManager = commandManager; this.condition = condition; this.configuration = configuration; this.onDataChanged = onDataChanged; eventQuestResolver = new EventQuestResolver(dataManager, log); framework.Update += OnFrameworkUpdate; log.Information("[EventQuest] Service initialized"); } public bool StartEventQuestRotation(string eventQuestId, List characters) { if (characters == null || characters.Count == 0) { log.Error("[EventQuest] Cannot start rotation: No characters selected"); return false; } if (string.IsNullOrEmpty(eventQuestId)) { log.Error("[EventQuest] Cannot start rotation: Event Quest ID is empty"); return false; } List dependencies = eventQuestResolver.ResolveEventQuestDependencies(eventQuestId); List remainingChars = new List(); List completedChars = new List(); foreach (string character in characters) { if (HasCharacterCompletedEventQuest(eventQuestId, character)) { completedChars.Add(character); log.Debug("[EventQuest] " + character + " already completed event quest " + eventQuestId); } else { remainingChars.Add(character); log.Debug("[EventQuest] " + character + " needs to complete event quest " + eventQuestId); } } if (remainingChars.Count == 0) { log.Information("[EventQuest] All characters have already completed event quest " + eventQuestId); return false; } string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter(); bool isAlreadyLoggedIn = !string.IsNullOrEmpty(currentLoggedInChar) && remainingChars.Contains(currentLoggedInChar); currentState = new EventQuestState { EventQuestId = eventQuestId, EventQuestName = eventQuestResolver.GetQuestName(eventQuestId), SelectedCharacters = new List(characters), RemainingCharacters = remainingChars, CompletedCharacters = completedChars, DependencyQuests = dependencies, Phase = ((!isAlreadyLoggedIn) ? EventQuestPhase.InitializingFirstCharacter : EventQuestPhase.CheckingQuestCompletion), CurrentCharacter = (isAlreadyLoggedIn ? currentLoggedInChar : ""), PhaseStartTime = DateTime.Now, RotationStartTime = DateTime.Now }; isRotationActive = true; log.Information("[EventQuest] ═══ Starting Event Quest Rotation ═══"); log.Information($"[EventQuest] Event Quest: {currentState.EventQuestName} ({eventQuestId})"); log.Information($"[EventQuest] Total Characters: {characters.Count}"); log.Information($"[EventQuest] Remaining: {remainingChars.Count} | Completed: {completedChars.Count}"); log.Information($"[EventQuest] Dependencies to resolve: {dependencies.Count}"); if (dependencies.Count > 0) { log.Information("[EventQuest] Prerequisites: " + string.Join(", ", dependencies.Select((string id) => eventQuestResolver.GetQuestName(id)))); } if (isAlreadyLoggedIn) { log.Information("[EventQuest] User already logged in as " + currentLoggedInChar + " - starting immediately"); } return true; } public EventQuestState GetCurrentState() { return currentState; } public void LoadEventQuestCompletionData(Dictionary> data) { if (data != null && data.Count > 0) { eventQuestCompletionByCharacter = new Dictionary>(data); log.Information($"[EventQuest] Loaded completion data for {data.Count} event quests"); } } public Dictionary> GetEventQuestCompletionData() { return new Dictionary>(eventQuestCompletionByCharacter); } public void AbortRotation() { log.Information("[EventQuest] Aborting Event Quest rotation"); currentState = new EventQuestState { Phase = EventQuestPhase.Idle }; isRotationActive = false; } private void MarkEventQuestCompleted(string eventQuestId, string characterName) { if (!eventQuestCompletionByCharacter.ContainsKey(eventQuestId)) { eventQuestCompletionByCharacter[eventQuestId] = new List(); } if (!eventQuestCompletionByCharacter[eventQuestId].Contains(characterName)) { eventQuestCompletionByCharacter[eventQuestId].Add(characterName); log.Debug("[EventQuest] Marked " + characterName + " as completed event quest " + eventQuestId); onDataChanged?.Invoke(); } } private bool HasCharacterCompletedEventQuest(string eventQuestId, string characterName) { if (eventQuestCompletionByCharacter.TryGetValue(eventQuestId, out List characters)) { return characters.Contains(characterName); } return false; } private void OnFrameworkUpdate(IFramework framework) { if (!isRotationActive) { return; } DateTime now = DateTime.Now; if (!((now - lastCheckTime).TotalMilliseconds < 250.0)) { lastCheckTime = now; CheckForTerritoryWait(); switch (currentState.Phase) { case EventQuestPhase.InitializingFirstCharacter: HandleInitializingFirstCharacter(); break; case EventQuestPhase.WaitingForCharacterLogin: HandleWaitingForCharacterLogin(); break; case EventQuestPhase.CheckingQuestCompletion: HandleCheckingQuestCompletion(); break; case EventQuestPhase.ResolvingDependencies: HandleResolvingDependencies(); break; case EventQuestPhase.ExecutingDependencies: HandleExecutingDependencies(); break; case EventQuestPhase.WaitingForQuestStart: case EventQuestPhase.QuestActive: HandleQuestMonitoring(); break; case EventQuestPhase.WaitingBeforeCharacterSwitch: HandleWaitingBeforeCharacterSwitch(); break; case EventQuestPhase.Completed: HandleCompleted(); break; } } } private void CheckForTerritoryWait() { if (!questionableIPC.IsRunning()) { return; } object task = questionableIPC.GetCurrentTask(); if (task == null) { return; } try { if (!(task is JObject jObject)) { return; } JToken taskNameToken = jObject["TaskName"]; if (taskNameToken == null) { return; } string taskName = taskNameToken.ToString(); if (string.IsNullOrEmpty(taskName)) { return; } Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName); if (!waitTerritoryMatch.Success) { return; } string territoryName = waitTerritoryMatch.Groups[1].Value.Trim(); uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value); string territoryKey = $"{territoryName}_{territoryId}"; double timeSinceLastTeleport = (DateTime.Now - lastTerritoryTeleportTime).TotalSeconds; if (lastTerritoryWaitDetected == territoryKey && timeSinceLastTeleport < 60.0) { return; } log.Information($"[EventQuest] Wait(territory) detected: {territoryName} (ID: {territoryId})"); log.Information("[EventQuest] Auto-teleporting via Lifestream..."); lastTerritoryWaitDetected = territoryKey; lastTerritoryTeleportTime = DateTime.Now; framework.RunOnFrameworkThread(delegate { try { string text = "/li " + territoryName; commandManager.ProcessCommand(text); log.Information("[EventQuest] Sent teleport command: " + text); } catch (Exception ex2) { log.Error("[EventQuest] Failed to teleport to " + territoryName + ": " + ex2.Message); } }); } catch (Exception ex) { log.Error("[EventQuest] Error checking Wait(territory) task: " + ex.Message); } } private void HandleInitializingFirstCharacter() { if (currentState.RemainingCharacters.Count == 0) { log.Information("[EventQuest] No remaining characters - rotation complete"); currentState.Phase = EventQuestPhase.Completed; isRotationActive = false; return; } string firstChar = currentState.RemainingCharacters[0]; currentState.CurrentCharacter = firstChar; log.Information("[EventQuest] >>> Initializing first character: " + firstChar); if (autoRetainerIpc.SwitchCharacter(firstChar)) { currentState.Phase = EventQuestPhase.WaitingForCharacterLogin; currentState.PhaseStartTime = DateTime.Now; log.Information("[EventQuest] Character switch initiated to " + firstChar); } else { log.Error("[EventQuest] Failed to switch to " + firstChar); currentState.Phase = EventQuestPhase.Error; currentState.ErrorMessage = "Failed to switch to " + firstChar; } } private void HandleWaitingForCharacterLogin() { if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds > 60.0) { log.Error("[EventQuest] Login timeout for " + currentState.CurrentCharacter); SkipToNextCharacter(); return; } string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter(); if (!string.IsNullOrEmpty(currentLoggedInChar) && currentLoggedInChar == currentState.CurrentCharacter && !((DateTime.Now - currentState.PhaseStartTime).TotalSeconds < 5.0)) { log.Information("[EventQuest] Successfully logged in as " + currentLoggedInChar); currentState.Phase = EventQuestPhase.CheckingQuestCompletion; currentState.PhaseStartTime = DateTime.Now; } } private void HandleCheckingQuestCompletion() { string eventQuestId = currentState.EventQuestId; string rawId = QuestIdParser.ParseQuestId(eventQuestId).rawId; QuestIdType questType = QuestIdParser.ClassifyQuestId(eventQuestId); log.Debug($"[EventQuest] Checking completion for {eventQuestId} (Type: {questType}, RawId: {rawId})"); if (!uint.TryParse(rawId, out var questIdUint)) { log.Error($"[EventQuest] Invalid quest ID: {eventQuestId} (cannot parse numeric part: {rawId})"); SkipToNextCharacter(); return; } bool isQuestComplete = false; try { isQuestComplete = QuestManager.IsQuestComplete(questIdUint); } catch (Exception ex) { log.Error("[EventQuest] Error checking quest completion: " + ex.Message); } if (isQuestComplete) { log.Information("[EventQuest] " + currentState.CurrentCharacter + " already completed event quest " + eventQuestId); List completedList = currentState.CompletedCharacters; if (!completedList.Contains(currentState.CurrentCharacter)) { completedList.Add(currentState.CurrentCharacter); currentState.CompletedCharacters = completedList; } MarkEventQuestCompleted(eventQuestId, currentState.CurrentCharacter); SkipToNextCharacter(); } else { log.Information("[EventQuest] " + currentState.CurrentCharacter + " needs to complete event quest " + eventQuestId); log.Information($"[EventQuest] >>> Starting event quest with {currentState.DependencyQuests.Count} prerequisites"); StartEventQuest(); } } private void HandleResolvingDependencies() { log.Information("[EventQuest] All prerequisites completed - starting event quest"); StartEventQuest(); } private void HandleExecutingDependencies() { string depQuestId = currentState.CurrentExecutingQuest; if (!uint.TryParse(depQuestId, out var questIdUint)) { log.Error("[EventQuest] Invalid dependency quest ID: " + depQuestId); currentState.DependencyIndex++; currentState.Phase = EventQuestPhase.ResolvingDependencies; return; } bool isDependencyComplete = false; try { isDependencyComplete = QuestManager.IsQuestComplete(questIdUint); } catch { } if (isDependencyComplete) { log.Information("[EventQuest] Dependency " + eventQuestResolver.GetQuestName(depQuestId) + " already completed"); currentState.DependencyIndex++; currentState.Phase = EventQuestPhase.ResolvingDependencies; return; } try { commandManager.ProcessCommand("/qst start"); log.Information("[EventQuest] Started dependency quest: " + eventQuestResolver.GetQuestName(depQuestId)); } catch (Exception ex) { log.Error("[EventQuest] Failed to start dependency: " + ex.Message); } currentState.Phase = EventQuestPhase.QuestActive; currentState.HasEventQuestBeenAccepted = false; currentState.PhaseStartTime = DateTime.Now; } private void HandleQuestMonitoring() { string eventQuestId = currentState.EventQuestId; try { if (questionableIPC.IsQuestComplete(eventQuestId)) { log.Information("[EventQuest] Event quest " + eventQuestId + " completed by " + currentState.CurrentCharacter); MarkEventQuestCompleted(currentState.EventQuestId, currentState.CurrentCharacter); List completedList = currentState.CompletedCharacters; if (!completedList.Contains(currentState.CurrentCharacter)) { completedList.Add(currentState.CurrentCharacter); currentState.CompletedCharacters = completedList; } try { commandManager.ProcessCommand("/qst stop"); log.Information("[EventQuest] Sent /qst stop"); } catch { } currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch; currentState.PhaseStartTime = DateTime.Now; } } catch (Exception ex) { log.Error("[EventQuest] Error checking quest completion via IPC: " + ex.Message); } } private void HandleWaitingBeforeCharacterSwitch() { if (!condition[ConditionFlag.BetweenAreas] && (DateTime.Now - currentState.PhaseStartTime).TotalSeconds >= 2.0) { PerformCharacterSwitch(); } } private void HandleCompleted() { log.Information("[EventQuest] ═══ EVENT QUEST ROTATION COMPLETED ═══"); log.Information($"[EventQuest] All {currentState.CompletedCharacters.Count} characters completed the event quest"); if (questionableIPC.IsAvailable) { try { questionableIPC.ClearQuestPriority(); log.Information("[EventQuest] Cleared quest priority queue after completion"); } catch (Exception ex) { log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message); } } isRotationActive = false; currentState.Phase = EventQuestPhase.Idle; } private void StartEventQuest() { List allQuests = new List(); if (currentState.DependencyQuests.Count > 0) { foreach (string dep in currentState.DependencyQuests) { allQuests.Add(dep); QuestIdType questType = QuestIdParser.ClassifyQuestId(dep); log.Information($"[EventQuest] Adding dependency: {dep} (Type: {questType})"); } } string mainQuestId = currentState.EventQuestId; allQuests.Add(mainQuestId); QuestIdType mainQuestType = QuestIdParser.ClassifyQuestId(mainQuestId); log.Information($"[EventQuest] Adding main event quest: {mainQuestId} (Type: {mainQuestType})"); log.Information($"[EventQuest] Setting {allQuests.Count} quests as Questionable priority"); if (questionableIPC.IsAvailable) { try { questionableIPC.ClearQuestPriority(); log.Information("[EventQuest] Cleared existing quest priority queue"); } catch (Exception ex) { log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message); } foreach (string questId in allQuests) { try { bool result = questionableIPC.AddQuestPriority(questId); log.Information($"[EventQuest] Added quest {questId} to priority: {result}"); } catch (Exception ex2) { log.Warning("[EventQuest] Failed to add quest " + questId + " to priority: " + ex2.Message); } } } else { log.Warning("[EventQuest] Questionable IPC not available - cannot set priority"); } if (condition[ConditionFlag.BetweenAreas]) { log.Debug("[EventQuest] Character is between areas - waiting before starting quest"); return; } if (questionableIPC.IsAvailable && questionableIPC.IsRunning()) { log.Debug("[EventQuest] Questionable is busy - waiting before starting quest"); return; } try { commandManager.ProcessCommand("/qst start"); log.Information("[EventQuest] Sent /qst start for event quest"); currentState.Phase = EventQuestPhase.QuestActive; currentState.CurrentExecutingQuest = currentState.EventQuestId; currentState.HasEventQuestBeenAccepted = false; currentState.PhaseStartTime = DateTime.Now; } catch (Exception ex3) { log.Error("[EventQuest] Failed to start quest: " + ex3.Message); } } private void SkipToNextCharacter() { try { commandManager.ProcessCommand("/qst stop"); log.Information("[EventQuest] Sent /qst stop before character switch"); } catch { } List remainingList = currentState.RemainingCharacters; List completedList = currentState.CompletedCharacters; if (remainingList.Contains(currentState.CurrentCharacter)) { remainingList.Remove(currentState.CurrentCharacter); currentState.RemainingCharacters = remainingList; } if (!completedList.Contains(currentState.CurrentCharacter)) { completedList.Add(currentState.CurrentCharacter); currentState.CompletedCharacters = completedList; log.Information("[EventQuest] Character " + currentState.CurrentCharacter + " marked as completed (skipped)"); } currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch; currentState.PhaseStartTime = DateTime.Now; } private void PerformCharacterSwitch() { List remainingList = currentState.RemainingCharacters; if (remainingList.Contains(currentState.CurrentCharacter)) { remainingList.Remove(currentState.CurrentCharacter); currentState.RemainingCharacters = remainingList; } if (currentState.RemainingCharacters.Count == 0) { currentState.Phase = EventQuestPhase.Completed; return; } string nextChar = currentState.RemainingCharacters[0]; currentState.CurrentCharacter = nextChar; currentState.NextCharacter = nextChar; log.Information("[EventQuest] Switching to next character: " + nextChar); log.Information($"[EventQuest] Progress: {currentState.CompletedCharacters.Count}/{currentState.SelectedCharacters.Count} completed"); if (autoRetainerIpc.SwitchCharacter(nextChar)) { currentState.Phase = EventQuestPhase.WaitingForCharacterLogin; currentState.PhaseStartTime = DateTime.Now; } else { log.Error("[EventQuest] Failed to switch to " + nextChar); currentState.Phase = EventQuestPhase.Error; currentState.ErrorMessage = "Failed to switch character"; } } public void Dispose() { framework.Update -= OnFrameworkUpdate; log.Information("[EventQuest] Service disposed"); } }