using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using QuestionableCompanion.Models; namespace QuestionableCompanion.Services; public class QuestRotationExecutionService : IDisposable { private readonly AutoRetainerIPC autoRetainerIpc; private readonly QuestTrackingService questTrackingService; private readonly IPluginLog log; private readonly IFramework framework; private readonly ICommandManager commandManager; private readonly ICondition condition; private readonly IClientState clientState; private readonly SubmarineManager submarineManager; private readonly QuestionableIPC questionableIPC; private readonly Configuration configuration; private DCTravelService? dcTravelService; private CharacterSafeWaitService? safeWaitService; private QuestPreCheckService? preCheckService; private MovementMonitorService? movementMonitor; private CombatDutyDetectionService? combatDutyDetection; private DeathHandlerService? deathHandler; private DungeonAutomationService? dungeonAutomation; private StepsOfFaithHandler? stepsOfFaithHandler; private ErrorRecoveryService? errorRecoveryService; private readonly List stopPoints = new List(); private RotationState currentState = new RotationState(); private Dictionary> questCompletionByCharacter = new Dictionary>(); private Action? onDataChanged; private DateTime lastCheckTime = DateTime.MinValue; private const double CheckIntervalMs = 250.0; private DateTime lastSubmarineCheckTime = DateTime.MinValue; private bool waitingForQuestAcceptForSubmarines; private uint? lastSoloDutyQuestId; private const double CharacterLoginTimeoutSeconds = 60.0; private const double PhaseTimeoutSeconds = 120.0; private bool isRotationActive; public bool IsRotationActive => isRotationActive; public QuestRotationExecutionService(AutoRetainerIPC autoRetainerIpc, QuestTrackingService questTrackingService, SubmarineManager submarineManager, QuestionableIPC questionableIPC, Configuration configuration, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, IClientState clientState, Action? onDataChanged = null) { this.autoRetainerIpc = autoRetainerIpc; this.questTrackingService = questTrackingService; this.submarineManager = submarineManager; this.questionableIPC = questionableIPC; this.configuration = configuration; this.log = log; this.framework = framework; this.commandManager = commandManager; this.condition = condition; this.clientState = clientState; this.onDataChanged = onDataChanged; framework.Update += OnFrameworkUpdate; log.Information("[QuestRotation] Service initialized"); } public bool AddStopPoint(uint questId, byte? sequence = null) { if (stopPoints.Any((StopPoint sp) => sp.QuestId == questId && sp.Sequence == sequence)) { log.Warning($"[QuestRotation] Stop point {questId}" + (sequence.HasValue ? $" Seq {sequence.Value}" : "") + " already exists"); return false; } StopPoint stopPoint = new StopPoint { QuestId = questId, Sequence = sequence, IsActive = false, CreatedAt = DateTime.Now }; stopPoints.Add(stopPoint); log.Information($"[QuestRotation] Added stop point: Quest {questId}"); return true; } public void ImportStopPointsFromQuestionable() { try { log.Information("[QuestRotation] Importing stop points from Questionable..."); List stopQuestIds = questionableIPC.GetStopQuestList(); Dictionary sequenceConditions = null; try { sequenceConditions = questionableIPC.GetAllQuestSequenceStopConditions(); } catch (Exception ex) { log.Error("[QuestRotation] Wrong Questionable Version "); log.Error("[QuestRotation] Import failed: " + ex.Message); return; } stopPoints.Clear(); if (stopQuestIds != null) { foreach (string questIdStr in stopQuestIds) { if (!uint.TryParse(questIdStr, out var questId)) { continue; } byte? sequence = null; if (sequenceConditions != null && sequenceConditions.ContainsKey(questIdStr)) { try { object seqObj = sequenceConditions[questId.ToString()]; if (seqObj != null) { sequence = Convert.ToByte(seqObj); } } catch { } } stopPoints.Add(new StopPoint { QuestId = questId, Sequence = sequence, IsActive = false, CreatedAt = DateTime.Now }); } } if (sequenceConditions != null) { foreach (KeyValuePair kvp in sequenceConditions) { if (uint.TryParse(kvp.Key, out var questId2) && !stopPoints.Any((StopPoint sp) => sp.QuestId == questId2)) { byte? sequence2 = null; try { sequence2 = Convert.ToByte(kvp.Value); } catch { } stopPoints.Add(new StopPoint { QuestId = questId2, Sequence = sequence2, IsActive = false, CreatedAt = DateTime.Now }); } } } log.Information($"[QuestRotation] Imported {stopPoints.Count} stop points"); } catch (Exception ex2) { log.Error("[QuestRotation] Failed to import stop points: " + ex2.Message); } } public bool RemoveStopPoint(uint questId) { if (isRotationActive) { log.Error($"[QuestRotation] Cannot remove stop point {questId} during active rotation!"); return false; } StopPoint stopPoint = stopPoints.FirstOrDefault((StopPoint sp) => sp.QuestId == questId); if (stopPoint == null) { log.Warning($"[QuestRotation] Stop point {questId} not found"); return false; } stopPoints.Remove(stopPoint); log.Information($"[QuestRotation] Removed stop point: Quest {questId}"); return true; } public bool StartRotation(uint questId, List characters) { if (characters == null || characters.Count == 0) { log.Error("[QuestRotation] Cannot start rotation: No characters selected"); return false; } StopPoint stopPoint = stopPoints.FirstOrDefault((StopPoint sp) => sp.QuestId == questId); if (stopPoint == null) { log.Error($"[QuestRotation] Cannot start rotation: Quest {questId} not in stop points"); return false; } log.Information("[QuestRotation] Found stop point: " + stopPoint.DisplayName); List remainingChars = new List(); List completedChars = new List(); if (questCompletionByCharacter.TryGetValue(questId, out List savedCompletedChars)) { log.Debug($"[QuestRotation] Quest {questId} has {savedCompletedChars.Count} characters marked as completed in saved data"); } else { log.Debug($"[QuestRotation] Quest {questId} has NO saved completion data"); } foreach (string character in characters) { if (HasCharacterCompletedQuest(questId, character)) { completedChars.Add(character); log.Debug($"[QuestRotation] {character} already completed quest {questId} (from saved data)"); } else { remainingChars.Add(character); log.Debug($"[QuestRotation] {character} needs to complete quest {questId}"); } } if (remainingChars.Count == 0) { log.Information($"[QuestRotation] All characters have already completed quest {questId}"); return false; } currentState = new RotationState { CurrentStopQuestId = questId, SelectedCharacters = new List(characters), RemainingCharacters = remainingChars, CompletedCharacters = completedChars, Phase = RotationPhase.InitializingFirstCharacter, PhaseStartTime = DateTime.Now, RotationStartTime = DateTime.Now, HasQuestBeenAccepted = false }; stopPoint.IsActive = true; isRotationActive = true; combatDutyDetection?.SetRotationActive(active: true); deathHandler?.SetRotationActive(active: true); if (configuration.EnableMovementMonitor && movementMonitor != null && !movementMonitor.IsMonitoring) { movementMonitor.StartMonitoring(); log.Information("[QuestRotation] Movement monitor started"); } log.Information("[QuestRotation] ═══ Starting Rotation ═══"); log.Information($"[QuestRotation] Quest ID: {questId}"); log.Information($"[QuestRotation] Total Characters: {characters.Count}"); log.Information($"[QuestRotation] Remaining: {remainingChars.Count} | Completed: {completedChars.Count}"); log.Information("[QuestRotation] Characters to process: " + string.Join(", ", remainingChars)); return true; } public bool StartRotationLevelOnly(List characters) { if (characters == null || characters.Count == 0) { log.Error("[QuestRotation] Cannot start rotation: No characters selected"); return false; } StopConditionData levelStopCondition = questionableIPC.GetLevelStopCondition(); if (levelStopCondition == null || !levelStopCondition.Enabled) { log.Error("[QuestRotation] Cannot start level-only rotation: Level stop condition not configured"); return false; } log.Information($"[QuestRotation] Starting level-only rotation (target level: {levelStopCondition.TargetValue})"); List remainingChars = new List(characters); currentState = new RotationState { CurrentStopQuestId = 0u, SelectedCharacters = new List(characters), RemainingCharacters = remainingChars, CompletedCharacters = new List(), Phase = RotationPhase.InitializingFirstCharacter, PhaseStartTime = DateTime.Now, RotationStartTime = DateTime.Now, HasQuestBeenAccepted = false }; isRotationActive = true; combatDutyDetection?.SetRotationActive(active: true); deathHandler?.SetRotationActive(active: true); if (configuration.EnableMovementMonitor && movementMonitor != null && !movementMonitor.IsMonitoring) { movementMonitor.StartMonitoring(); log.Information("[QuestRotation] Movement monitor started"); } log.Information("[QuestRotation] ═══ Starting Level-Only Rotation ═══"); log.Information($"[QuestRotation] Target Level: {levelStopCondition.TargetValue}"); log.Information($"[QuestRotation] Total Characters: {characters.Count}"); log.Information("[QuestRotation] Characters to process: " + string.Join(", ", remainingChars)); return true; } public bool StartSyncRotation(List characters) { if (characters == null || characters.Count == 0) { log.Error("[QuestRotation] Cannot start sync rotation: No characters provided"); return false; } List charactersToSync = new List(); foreach (string character in characters) { if (GetCompletedQuestsByCharacter(character).Count == 0) { charactersToSync.Add(character); } } if (charactersToSync.Count == 0) { log.Information("[QuestRotation] No characters need sync - all have existing data"); return false; } log.Information("[QuestRotation] ═══ Starting Sync Rotation ═══"); log.Information($"[QuestRotation] Characters to sync: {charactersToSync.Count}"); log.Information("[QuestRotation] Characters: " + string.Join(", ", charactersToSync)); currentState = new RotationState { CurrentStopQuestId = 0u, SelectedCharacters = new List(charactersToSync), RemainingCharacters = new List(charactersToSync), CompletedCharacters = new List(), Phase = RotationPhase.InitializingFirstCharacter, PhaseStartTime = DateTime.Now, RotationStartTime = DateTime.Now, HasQuestBeenAccepted = false, IsSyncOnlyMode = true }; isRotationActive = true; log.Information("[QuestRotation] Sync rotation started successfully!"); return true; } public RotationState GetCurrentState() { return currentState; } public (int completed, int total) GetRotationProgress(uint questId) { if (currentState.SelectedCharacters.Count == 0) { return (completed: 0, total: 0); } return GetRotationProgress(questId, currentState.SelectedCharacters); } public (int completed, int total) GetRotationProgress(uint questId, List characters) { if (characters == null || characters.Count == 0) { return (completed: 0, total: 0); } int completed = 0; foreach (string character in characters) { if (HasCharacterCompletedQuest(questId, character)) { completed++; } } return (completed: completed, total: characters.Count); } public List GetAllStopPoints() { return new List(stopPoints); } public void LoadQuestCompletionData(Dictionary> data) { if (data != null && data.Count > 0) { questCompletionByCharacter = new Dictionary>(data); log.Information($"[QuestRotation] Loaded quest completion data for {data.Count} quests"); int count = 0; foreach (KeyValuePair> kvp in data) { if (count < 5) { log.Information($"[QuestRotation] DEBUG: Quest {kvp.Key} -> {kvp.Value.Count} characters: {string.Join(", ", kvp.Value)}"); count++; } } if (data.Count > 5) { log.Information($"[QuestRotation] DEBUG: ... and {data.Count - 5} more quests"); } } else { log.Information("[QuestRotation] No quest completion data to load (empty or null)"); } } public Dictionary> GetQuestCompletionData() { return new Dictionary>(questCompletionByCharacter); } public void SetDCTravelService(DCTravelService service) { dcTravelService = service; log.Information("[QuestRotation] DC Travel service linked"); } public void SetSafeWaitService(CharacterSafeWaitService service) { safeWaitService = service; log.Information("[QuestRotation] Safe Wait service linked"); } public void SetPreCheckService(QuestPreCheckService service) { preCheckService = service; log.Information("[QuestRotation] PreCheck service linked"); } public void SetMovementMonitor(MovementMonitorService service) { movementMonitor = service; log.Information("[QuestRotation] Movement Monitor service linked"); } public void SetCombatDutyDetection(CombatDutyDetectionService service) { combatDutyDetection = service; log.Information("[QuestRotation] Combat/Duty Detection service linked"); } public void SetDeathHandler(DeathHandlerService service) { deathHandler = service; log.Information("[QuestRotation] Death Handler service linked"); } public void SetDungeonAutomation(DungeonAutomationService service) { dungeonAutomation = service; log.Information("[QuestRotation] Dungeon Automation service linked"); } public void SetStepsOfFaithHandler(StepsOfFaithHandler service) { stepsOfFaithHandler = service; log.Information("[QuestRotation] Steps of Faith Handler service linked"); } public void SetErrorRecoveryService(ErrorRecoveryService service) { errorRecoveryService = service; log.Information("[QuestRotation] Error Recovery service linked"); } private void MarkQuestCompleted(uint questId, string characterName) { if (!questCompletionByCharacter.ContainsKey(questId)) { questCompletionByCharacter[questId] = new List(); } if (!questCompletionByCharacter[questId].Contains(characterName)) { questCompletionByCharacter[questId].Add(characterName); log.Debug($"[QuestRotation] Marked {characterName} as completed quest {questId}"); onDataChanged?.Invoke(); } } public List GetCompletedQuestsByCharacter(string characterName) { List completedQuests = new List(); foreach (KeyValuePair> kvp in questCompletionByCharacter) { if (kvp.Value.Contains(characterName)) { completedQuests.Add(kvp.Key); } } if (preCheckService != null) { foreach (uint questId in preCheckService.GetCompletedQuests(characterName)) { if (!completedQuests.Contains(questId)) { completedQuests.Add(questId); } } } return completedQuests; } private bool HasCharacterCompletedQuest(uint questId, string characterName) { if (questCompletionByCharacter.TryGetValue(questId, out List characters) && characters.Contains(characterName)) { return true; } if (preCheckService != null) { bool? status = preCheckService.GetQuestStatus(characterName, questId); if (status.HasValue && status.Value) { return true; } } return false; } public void ClearCharacterQuestData(string characterName) { log.Information("[QuestRotation] Clearing all quest data for " + characterName); int questsCleared = 0; foreach (KeyValuePair> kvp in questCompletionByCharacter.ToList()) { if (kvp.Value.Remove(characterName)) { questsCleared++; } if (kvp.Value.Count == 0) { questCompletionByCharacter.Remove(kvp.Key); } } log.Information($"[QuestRotation] Removed {characterName} from {questsCleared} quests in rotation tracking"); if (preCheckService != null) { preCheckService.ClearCharacterData(characterName); log.Information("[QuestRotation] Cleared " + characterName + " data from PreCheck service"); } onDataChanged?.Invoke(); log.Information("[QuestRotation] Quest data reset complete for " + characterName); } private void ScanAndSaveAllCompletedQuests(string characterName) { if (string.IsNullOrEmpty(characterName)) { return; } int questsScanned = 0; int questsCompleted = 0; try { for (uint questId = 65536u; questId < 72000; questId++) { try { if (QuestManager.IsQuestComplete(questId)) { MarkQuestCompleted(questId, characterName); questsCompleted++; } questsScanned++; } catch { } } log.Information($"[QuestRotation] Scanned {questsScanned} quests, found {questsCompleted} completed for {characterName}"); onDataChanged?.Invoke(); framework.RunOnFrameworkThread(delegate { if (currentState.Phase == RotationPhase.ScanningQuests) { currentState.Phase = RotationPhase.CheckingQuestCompletion; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Quest scan complete - moving to quest check"); } }); } catch (Exception ex) { log.Error("[QuestRotation] Error scanning quests for " + characterName + ": " + ex.Message); } } public void SyncQuestDataForCurrentCharacter() { IPlayerCharacter player = clientState.LocalPlayer; if (player == null) { log.Warning("[QuestRotation] No character logged in - cannot sync quest data"); return; } string characterName = $"{player.Name}@{player.HomeWorld.Value.Name}"; log.Information("[QuestRotation] Starting quest data sync for " + characterName); ScanAndSaveAllCompletedQuests(characterName); } public void AbortRotation() { log.Information("[QuestRotation] Aborting rotation"); foreach (StopPoint stopPoint in stopPoints) { stopPoint.IsActive = false; } currentState = new RotationState { Phase = RotationPhase.Idle }; isRotationActive = false; combatDutyDetection?.SetRotationActive(active: false); combatDutyDetection?.Reset(); deathHandler?.SetRotationActive(active: false); deathHandler?.Reset(); if (dungeonAutomation != null) { dungeonAutomation.Reset(); log.Information("[QuestRotation] Dungeon automation reset"); } if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Information("[QuestRotation] Movement monitor stopped"); } } private void OnFrameworkUpdate(IFramework framework) { DateTime now = DateTime.Now; if ((now - lastCheckTime).TotalMilliseconds < 250.0) { return; } lastCheckTime = now; if (isRotationActive && errorRecoveryService != null && errorRecoveryService.IsErrorDisconnect) { string charToRelog = errorRecoveryService.LastDisconnectedCharacter ?? currentState.CurrentCharacter; if (!string.IsNullOrEmpty(charToRelog)) { log.Warning("[ErrorRecovery] Disconnect detected for " + charToRelog); log.Information("[ErrorRecovery] Automatically relogging to " + charToRelog + "..."); if (errorRecoveryService.RequestRelog()) { errorRecoveryService.Reset(); currentState.Phase = RotationPhase.WaitingForCharacterLogin; currentState.CurrentCharacter = charToRelog; currentState.PhaseStartTime = DateTime.Now; log.Information("[ErrorRecovery] Relog initiated for " + charToRelog); } else { log.Error("[ErrorRecovery] Failed to request relog via AutoRetainer"); errorRecoveryService.Reset(); } return; } log.Warning("[ErrorRecovery] Disconnect detected but no character to relog to"); errorRecoveryService.Reset(); } if (deathHandler != null && combatDutyDetection != null && !combatDutyDetection.IsInDuty) { deathHandler.Update(); } if (dungeonAutomation != null && !submarineManager.IsSubmarinePaused) { dungeonAutomation.Update(); if (isRotationActive && configuration.EnableAutoDutyUnsynced && !dungeonAutomation.IsWaitingForParty && currentState.Phase != RotationPhase.WaitingForCharacterLogin && currentState.Phase != RotationPhase.WaitingBeforeCharacterSwitch && currentState.Phase != RotationPhase.WaitingForHomeworldReturn && currentState.Phase != RotationPhase.ScanningQuests && currentState.Phase != RotationPhase.CheckingQuestCompletion && currentState.Phase != RotationPhase.InitializingFirstCharacter) { _ = submarineManager.IsSubmarinePaused; } } if (combatDutyDetection != null) { combatDutyDetection.Update(); if (combatDutyDetection.JustEnteredDuty && isRotationActive) { string currentQuestIdStr = questionableIPC.GetCurrentQuestId(); uint questId = 0u; if (!string.IsNullOrEmpty(currentQuestIdStr)) { uint.TryParse(currentQuestIdStr, out questId); } combatDutyDetection.SetCurrentQuestId(questId); bool isAutoDutyDungeon = dungeonAutomation != null && dungeonAutomation.IsInAutoDutyDungeon; combatDutyDetection.SetAutoDutyDungeon(isAutoDutyDungeon); if (isAutoDutyDungeon) { log.Information("[QuestRotation] AutoDuty Dungeon entered - /ad stop after 1s"); dungeonAutomation?.OnDutyEntered(); } else if (questId != 0) { bool shouldLog = lastSoloDutyQuestId != questId; if (questId == 811) { if (shouldLog) { log.Information("[QuestRotation] Quest 811 Solo Duty - stopping RSR, NO combat commands"); lastSoloDutyQuestId = questId; } } else if (shouldLog) { log.Information("[QuestRotation] Solo Duty entered - combat commands will activate after 8s"); lastSoloDutyQuestId = questId; } if (questId == 4591 && stepsOfFaithHandler != null) { IPlayerCharacter player = clientState.LocalPlayer; string characterName = ((player != null) ? $"{player.Name}@{player.HomeWorld.Value.Name}" : string.Empty); if (stepsOfFaithHandler.ShouldActivate(questId, isInSoloDuty: true)) { log.Information("[QuestRotation] Triggering Steps of Faith handler (will wait for conditions inside)..."); Task.Run(delegate { stepsOfFaithHandler.Execute(characterName); }); } } if (questId == 811) { try { commandManager.ProcessCommand("/rotation off"); log.Information("[QuestRotation] ✓ /rotation off sent for Quest 811"); } catch (Exception ex) { log.Error("[QuestRotation] Failed to stop RSR: " + ex.Message); } } } } if (combatDutyDetection.JustExitedDuty && isRotationActive && dungeonAutomation != null) { dungeonAutomation.OnDutyExited(); if ((DateTime.Now - combatDutyDetection.DutyExitTime).TotalSeconds >= 8.0) { if (dungeonAutomation.IsInAutoDutyDungeon) { dungeonAutomation.DisbandParty(); } try { commandManager.ProcessCommand("/qst start"); log.Information("[QuestRotation] ✓ /qst start (after duty exit)"); } catch (Exception ex2) { log.Error("[QuestRotation] Failed to restart quest: " + ex2.Message); } combatDutyDetection.ClearDutyExitFlag(); } } if (combatDutyDetection.ShouldPauseAutomation && movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Debug("[QuestRotation] Movement monitor paused for combat/duty"); } else if (!combatDutyDetection.ShouldPauseAutomation && movementMonitor != null && !movementMonitor.IsMonitoring && isRotationActive && configuration.EnableMovementMonitor && currentState.Phase != RotationPhase.WaitingForCharacterLogin && currentState.Phase != RotationPhase.WaitingBeforeCharacterSwitch && currentState.Phase != RotationPhase.WaitingForHomeworldReturn && currentState.Phase != RotationPhase.ScanningQuests && currentState.Phase != RotationPhase.CheckingQuestCompletion && currentState.Phase != RotationPhase.InitializingFirstCharacter) { movementMonitor.StartMonitoring(); log.Debug("[QuestRotation] Movement monitor resumed after combat/duty"); } } if (submarineManager.IsSubmarineJustCompleted && !submarineManager.IsSubmarineCooldownActive()) { log.Information("[QuestRotation] Submarine cooldown expired - re-enabling submarine checks"); submarineManager.ClearSubmarineJustCompleted(); } if (!isRotationActive) { return; } CheckLevelStopCondition(); switch (currentState.Phase) { case RotationPhase.InitializingFirstCharacter: HandleInitializingFirstCharacter(); break; case RotationPhase.WaitingForCharacterLogin: HandleWaitingForCharacterLogin(); break; case RotationPhase.ScanningQuests: HandleScanningQuests(); break; case RotationPhase.CheckingQuestCompletion: HandleCheckingQuestCompletion(); break; case RotationPhase.DCTraveling: if ((DateTime.Now - currentState.PhaseStartTime).TotalMinutes > 3.0) { log.Error("[QuestRotation] DC Travel timeout after 3 minutes - skipping character"); SkipToNextCharacter(); } break; case RotationPhase.WaitingForQuestStart: case RotationPhase.QuestActive: HandleQuestMonitoring(); break; case RotationPhase.WaitingBeforeCharacterSwitch: HandleWaitingBeforeCharacterSwitch(); break; case RotationPhase.WaitingForHomeworldReturn: HandleWaitingForHomeworldReturn(); break; case RotationPhase.Completed: HandleCompleted(); break; case RotationPhase.Questing: case RotationPhase.InCombat: case RotationPhase.InDungeon: case RotationPhase.HandlingSubmarines: case RotationPhase.SyncingCharacterData: case RotationPhase.WaitingForChauffeur: case RotationPhase.TravellingWithChauffeur: case RotationPhase.WaitingForNextCharacterSwitch: case RotationPhase.Error: break; } } private void CheckLevelStopCondition() { StopConditionData stopLevelData = questionableIPC.GetLevelStopCondition(); if (stopLevelData == null || !stopLevelData.Enabled) { return; } int stopLevel = stopLevelData.TargetValue; int currentLevel = 0; if (clientState.LocalPlayer != null) { currentLevel = clientState.LocalPlayer.Level; } if (currentLevel < stopLevel) { return; } log.Information($"[QuestRotation] Level Stop Condition reached (Level {currentLevel} >= {stopLevel})"); if (currentState.Phase == RotationPhase.Questing || currentState.Phase == RotationPhase.QuestActive || currentState.Phase == RotationPhase.WaitingForQuestStart) { log.Information("[QuestRotation] Level reached - stopping quest and switching character..."); try { commandManager.ProcessCommand("/qst stop"); log.Information("[QuestRotation] ✓ /qst stop (level stop condition)"); } catch (Exception ex) { log.Error("[QuestRotation] Failed to send /qst stop: " + ex.Message); } MarkCharacterCompleted(currentState.CurrentCharacter, "level reached"); currentState.Phase = RotationPhase.WaitingForHomeworldReturn; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Waiting 5s before homeworld return..."); } } private unsafe bool IsTerritoryLoaded() { GameMain* gameMain = GameMain.Instance(); if (gameMain == null) { return false; } return gameMain->TerritoryLoadState == 2; } private void HandleInitializingFirstCharacter() { if (currentState.RemainingCharacters.Count == 0) { log.Information("[QuestRotation] No remaining characters - rotation complete"); currentState.Phase = RotationPhase.Completed; isRotationActive = false; combatDutyDetection?.SetRotationActive(active: false); deathHandler?.SetRotationActive(active: false); return; } string firstChar = currentState.RemainingCharacters[0]; currentState.CurrentCharacter = firstChar; log.Information("[QuestRotation] >>> Phase 1: Initializing first character: " + firstChar); if (autoRetainerIpc.SwitchCharacter(firstChar)) { currentState.Phase = RotationPhase.WaitingForCharacterLogin; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Character switch initiated to " + firstChar); } else { log.Error("[QuestRotation] Failed to switch to " + firstChar); currentState.Phase = RotationPhase.Error; currentState.ErrorMessage = "Failed to switch to " + firstChar; } } private void HandleWaitingForCharacterLogin() { if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Debug("[QuestRotation] Movement monitor stopped during character login"); } if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds > 60.0) { log.Error("[QuestRotation] Login timeout for " + currentState.CurrentCharacter); SkipToNextCharacter(); return; } string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter(); if (string.IsNullOrEmpty(currentLoggedInChar) || !(currentLoggedInChar == currentState.CurrentCharacter) || (DateTime.Now - currentState.PhaseStartTime).TotalSeconds < 2.0 || (DateTime.Now - currentState.PhaseStartTime).TotalSeconds < 5.0) { return; } log.Information("[QuestRotation] >>> Phase 2: Successfully logged in as " + currentLoggedInChar); if (preCheckService != null) { log.Information("[QuestRotation] Scanning quest status for current character..."); try { preCheckService.ScanCurrentCharacterQuestStatus(); } catch (Exception ex) { log.Error("[QuestRotation] Error scanning quest status: " + ex.Message); } } currentState.Phase = RotationPhase.ScanningQuests; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Starting quest scan for " + currentState.CurrentCharacter + "..."); Task.Run(delegate { ScanAndSaveAllCompletedQuests(currentState.CurrentCharacter); }); } private void HandleScanningQuests() { if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds > 30.0) { log.Warning("[QuestRotation] Quest scan timeout - proceeding anyway"); currentState.Phase = RotationPhase.CheckingQuestCompletion; currentState.PhaseStartTime = DateTime.Now; } } private void HandleCheckingQuestCompletion() { uint questId = currentState.CurrentStopQuestId; bool isQuestComplete = false; try { isQuestComplete = QuestManager.IsQuestComplete(questId); } catch (Exception ex) { log.Error("[QuestRotation] Error checking quest completion: " + ex.Message); } if (isQuestComplete) { log.Information($"[QuestRotation] {currentState.CurrentCharacter} already completed quest {questId} - skipping"); MarkCharacterCompleted(currentState.CurrentCharacter, $"quest {questId} already complete"); MarkQuestCompleted(questId, currentState.CurrentCharacter); SkipToNextCharacter(); return; } if (questId == 0) { if (currentState.IsSyncOnlyMode) { log.Information("[QuestRotation] Sync-Only Mode: Quest scan complete for " + currentState.CurrentCharacter + " - moving to next character"); MarkCharacterCompleted(currentState.CurrentCharacter, "quest data synchronized"); SkipToNextCharacter(); return; } StopConditionData stopLevelData = questionableIPC.GetLevelStopCondition(); if (stopLevelData != null && stopLevelData.Enabled) { byte currentLevel = clientState.LocalPlayer?.Level ?? 0; if (currentLevel >= stopLevelData.TargetValue) { log.Information($"[QuestRotation] {currentState.CurrentCharacter} already at target level ({currentLevel} >= {stopLevelData.TargetValue}) - skipping"); MarkCharacterCompleted(currentState.CurrentCharacter, "target level reached"); SkipToNextCharacter(); return; } log.Information($"[QuestRotation] Level-Only Mode: {currentState.CurrentCharacter} at level {currentLevel}, targeting {stopLevelData.TargetValue}"); } } log.Information($"[QuestRotation] {currentState.CurrentCharacter} needs to complete quest {questId}"); if (configuration.EnableDCTravel && dcTravelService != null && dcTravelService.ShouldPerformDCTravel()) { log.Information("[QuestRotation] === DC TRAVEL REQUIRED ==="); log.Information("[QuestRotation] Performing DC travel before starting quest..."); currentState.Phase = RotationPhase.DCTraveling; currentState.PhaseStartTime = DateTime.Now; PerformDCTravelAndStartQuest(); return; } log.Information("[QuestRotation] >>> Phase 4: Waiting for quest to start..."); if (configuration.EnableMovementMonitor && movementMonitor != null && !movementMonitor.IsMonitoring) { movementMonitor.StartMonitoring(); log.Information("[QuestRotation] Movement monitor started for quest"); } try { commandManager.ProcessCommand("/qst start"); log.Information("[QuestRotation] Sent /qst start command"); } catch (Exception ex2) { log.Error("[QuestRotation] Failed to send /qst start: " + ex2.Message); } currentState.Phase = RotationPhase.WaitingForQuestStart; currentState.HasQuestBeenAccepted = false; currentState.PhaseStartTime = DateTime.Now; } private void PerformDCTravelAndStartQuest() { if (dcTravelService == null) { return; } Task.Run(async delegate { try { if (await dcTravelService.PerformDCTravel()) { log.Information("[QuestRotation] DC travel completed - starting quest"); try { commandManager.ProcessCommand("/qst start"); log.Information("[QuestRotation] Sent /qst start command"); } catch (Exception ex) { log.Error("[QuestRotation] Failed to send /qst start: " + ex.Message); } currentState.Phase = RotationPhase.WaitingForQuestStart; currentState.HasQuestBeenAccepted = false; currentState.PhaseStartTime = DateTime.Now; } else { log.Error("[QuestRotation] DC travel failed - skipping character"); SkipToNextCharacter(); } } catch (Exception ex2) { log.Error("[QuestRotation] DC travel error: " + ex2.Message); SkipToNextCharacter(); } }); } private unsafe void HandleQuestMonitoring() { uint questId = currentState.CurrentStopQuestId; bool hasReachedStopSequence = false; StopPoint stopPointForSubmarine = stopPoints.FirstOrDefault((StopPoint sp) => sp.QuestId == questId && sp.IsActive); if (stopPointForSubmarine != null && stopPointForSubmarine.Sequence.HasValue) { string currentQuestIdStr = questionableIPC.GetCurrentQuestId(); byte? currentSequence = questionableIPC.GetCurrentSequence(); if (!string.IsNullOrEmpty(currentQuestIdStr) && currentSequence.HasValue && uint.TryParse(currentQuestIdStr, out var currentQuestId) && currentQuestId == questId && currentSequence.Value >= stopPointForSubmarine.Sequence.Value) { hasReachedStopSequence = true; log.Debug($"[QuestRotation] Stop sequence reached (Quest {questId} Seq {currentSequence.Value} >= {stopPointForSubmarine.Sequence.Value}) - skipping submarine check"); } } if (!hasReachedStopSequence && !submarineManager.IsSubmarinePaused && !submarineManager.IsSubmarineCooldownActive()) { TimeSpan submarineCheckInterval = TimeSpan.FromSeconds(configuration.SubmarineCheckInterval); if (DateTime.Now - lastSubmarineCheckTime >= submarineCheckInterval) { lastSubmarineCheckTime = DateTime.Now; if (submarineManager.CheckSubmarines()) { log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] === SUBMARINES READY - WAITING FOR QUEST COMPLETION ==="); log.Information("[QuestRotation] ========================================"); waitingForQuestAcceptForSubmarines = true; log.Information("[QuestRotation] Waiting for current quest to complete, then will pause for submarines..."); return; } } } if (waitingForQuestAcceptForSubmarines) { if (QuestManager.Instance() != null && questionableIPC.GetCurrentSequence() == 0) { log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] === QUEST ACCEPTED - STOPPING QUESTIONABLE ==="); log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] Stopping Questionable..."); framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/qst stop"); }).Wait(); log.Information("[QuestRotation] ✓ /qst stop command sent"); log.Information("[QuestRotation] Enabling Multi-Mode..."); submarineManager.EnableMultiMode(); waitingForQuestAcceptForSubmarines = false; if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Information("[QuestRotation] Movement monitor stopped for submarine operations"); } log.Information("[QuestRotation] Multi-Mode enabled - submarines will now run"); } } else if (submarineManager.IsSubmarinePaused) { if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Debug("[QuestRotation] Movement monitor stopped during submarine multi-mode"); } log.Debug("[QuestRotation] Submarines running in Multi-Mode..."); int waitTime = submarineManager.CheckSubmarinesSoon(); if (waitTime == 0) { log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] === NO SUBMARINES IN NEXT 2 MINUTES ==="); log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] Disabling Multi-Mode and returning to character..."); submarineManager.DisableMultiModeAndReturn(); string currentChar = currentState.CurrentCharacter; log.Information("[QuestRotation] Relogging to " + currentChar + "..."); if (autoRetainerIpc.SwitchCharacter(currentChar)) { log.Information("[QuestRotation] Relog initiated - waiting for character login..."); submarineManager.CompleteSubmarineRelog(); if (configuration.EnableMovementMonitor && movementMonitor != null && !movementMonitor.IsMonitoring) { movementMonitor.StartMonitoring(); log.Information("[QuestRotation] Movement monitor restarted after submarine operations"); } log.Information("[QuestRotation] Questionable will auto-resume when character is ready"); } else { log.Error("[QuestRotation] Failed to relog to " + currentChar + "!"); } } else { log.Debug($"[QuestRotation] Submarine ready in {waitTime}s - waiting..."); } } else { if (submarineManager.IsSubmarineCooldownActive() || waitingForQuestAcceptForSubmarines) { return; } questTrackingService.UpdateCurrentCharacterQuests(currentState.CurrentCharacter); StopPoint activeStopPoint = stopPoints.FirstOrDefault((StopPoint sp) => sp.QuestId == questId && sp.IsActive); bool shouldRotate = false; if (activeStopPoint != null && activeStopPoint.Sequence.HasValue) { string currentQuestIdStr2 = questionableIPC.GetCurrentQuestId(); byte? currentSequence2 = questionableIPC.GetCurrentSequence(); uint currentQuestId2; if (string.IsNullOrEmpty(currentQuestIdStr2) && currentState.HasQuestBeenAccepted) { if (QuestManager.Instance() != null) { byte gameQuestSeq = QuestManager.GetQuestSequence((ushort)questId); if (gameQuestSeq >= activeStopPoint.Sequence.Value) { log.Information("[QuestRotation] ✓ Questionable auto-stopped at stop point!"); log.Information($"[QuestRotation] Quest {questId} Sequence {gameQuestSeq} >= {activeStopPoint.Sequence.Value}"); shouldRotate = true; } else { log.Debug($"[QuestRotation] Questionable stopped but not at stop sequence (seq {gameQuestSeq} < {activeStopPoint.Sequence.Value})"); } } } else if (!string.IsNullOrEmpty(currentQuestIdStr2) && currentSequence2.HasValue && uint.TryParse(currentQuestIdStr2, out currentQuestId2)) { if (currentQuestId2 == questId) { if (currentSequence2.Value >= activeStopPoint.Sequence.Value) { log.Information($"[QuestRotation] ✓ Quest {questId} Sequence {activeStopPoint.Sequence.Value} reached by {currentState.CurrentCharacter}!"); log.Information($"[QuestRotation] Current Sequence: {currentSequence2.Value} (reached {activeStopPoint.Sequence.Value})"); shouldRotate = true; } } else { bool isQuestComplete = false; try { isQuestComplete = QuestManager.IsQuestComplete(questId); } catch (Exception ex) { log.Error("[QuestRotation] Error checking quest completion: " + ex.Message); return; } if (isQuestComplete) { log.Information($"[QuestRotation] ✓ Quest {questId} completed by {currentState.CurrentCharacter}!"); shouldRotate = true; } } } } else { bool isQuestComplete2 = false; try { isQuestComplete2 = QuestManager.IsQuestComplete(questId); } catch (Exception ex2) { log.Error("[QuestRotation] Error checking quest completion: " + ex2.Message); return; } if (isQuestComplete2) { log.Information($"[QuestRotation] ✓ Quest {questId} completed by {currentState.CurrentCharacter}!"); shouldRotate = true; } } if (shouldRotate) { log.Information($"[QuestRotation] ✓ Quest {questId} completed by {currentState.CurrentCharacter}!"); if (stepsOfFaithHandler != null && questId == 4591) { stepsOfFaithHandler.Reset(); } try { commandManager.ProcessCommand("/qst stop"); log.Information("[QuestRotation] Sent /qst stop command"); } catch (Exception ex3) { log.Error("[QuestRotation] Failed to send /qst stop: " + ex3.Message); } log.Information("[QuestRotation] Updating quest completion data for " + currentState.CurrentCharacter + "..."); ScanAndSaveAllCompletedQuests(currentState.CurrentCharacter); List completed = currentState.CompletedCharacters; List remaining = currentState.RemainingCharacters; log.Debug($"[QuestRotation] DEBUG - Before update: CompletedCharacters count = {completed.Count}, RemainingCharacters count = {remaining.Count}"); if (!completed.Contains(currentState.CurrentCharacter)) { completed.Add(currentState.CurrentCharacter); log.Information("[QuestRotation] ✓ Added " + currentState.CurrentCharacter + " to CompletedCharacters list"); } else { log.Warning("[QuestRotation] " + currentState.CurrentCharacter + " was already in CompletedCharacters list!"); } bool wasRemoved = remaining.Remove(currentState.CurrentCharacter); log.Information($"[QuestRotation] Removed {currentState.CurrentCharacter} from RemainingCharacters list: {wasRemoved}"); currentState.CompletedCharacters = completed; currentState.RemainingCharacters = remaining; log.Debug($"[QuestRotation] DEBUG - After update: CompletedCharacters count = {currentState.CompletedCharacters.Count}, RemainingCharacters count = {currentState.RemainingCharacters.Count}"); log.Debug("[QuestRotation] DEBUG - CompletedCharacters: " + string.Join(", ", currentState.CompletedCharacters)); log.Debug("[QuestRotation] DEBUG - RemainingCharacters: " + string.Join(", ", currentState.RemainingCharacters)); currentState.Phase = RotationPhase.WaitingForHomeworldReturn; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Waiting 5s before homeworld return..."); } else { if (currentState.Phase != RotationPhase.WaitingForQuestStart) { return; } bool isQuestAccepted = false; try { QuestManager* questManager = QuestManager.Instance(); if (questManager != null) { isQuestAccepted = questManager->IsQuestAccepted((ushort)questId); } } catch { } if (isQuestAccepted) { log.Information($"[QuestRotation] Quest {questId} accepted by {currentState.CurrentCharacter} - now monitoring for completion"); currentState.Phase = RotationPhase.QuestActive; currentState.HasQuestBeenAccepted = true; } } } } private void HandleCompleted() { log.Information("[QuestRotation] ═══ ROTATION COMPLETED ═══"); log.Information($"[QuestRotation] All {currentState.CompletedCharacters.Count} characters completed quest {currentState.CurrentStopQuestId}"); if (dcTravelService != null && dcTravelService.IsDCTravelCompleted()) { log.Information("[QuestRotation] DC Travel state reset after rotation completion"); dcTravelService.ResetDCTravelState(); } StopPoint stopPoint = stopPoints.FirstOrDefault((StopPoint sp) => sp.QuestId == currentState.CurrentStopQuestId); if (stopPoint != null) { stopPoint.IsActive = false; } isRotationActive = false; combatDutyDetection?.SetRotationActive(active: false); combatDutyDetection?.Reset(); deathHandler?.SetRotationActive(active: false); deathHandler?.Reset(); if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Information("[QuestRotation] Movement monitor stopped"); } currentState.Phase = RotationPhase.Idle; } private void HandleWaitingForHomeworldReturn() { if (!((DateTime.Now - currentState.PhaseStartTime).TotalSeconds >= 5.0)) { return; } if (configuration.ReturnToHomeworldOnStopQuest) { log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] === SENDING HOMEWORLD RETURN COMMAND ==="); log.Information("[QuestRotation] ========================================"); try { commandManager.ProcessCommand("/li"); log.Information("[QuestRotation] ✓ /li command sent (homeworld return)"); } catch (Exception ex) { log.Error("[QuestRotation] Failed to send /li command: " + ex.Message); } } else { log.Information("[QuestRotation] Skipping homeworld return (setting disabled)"); } Task.Delay(2000).ContinueWith(delegate { framework.RunOnFrameworkThread(delegate { log.Information("[QuestRotation] Homeworld return initiated, moving to next character..."); SkipToNextCharacter(); }); }); } private void SkipToNextCharacter() { if (preCheckService != null) { log.Information("[QuestRotation] Logging completed quests before logout..."); preCheckService.LogCompletedQuestsBeforeLogout(); } if (dcTravelService != null && dcTravelService.IsDCTravelCompleted()) { log.Information("[QuestRotation] DC Travel state reset for next character"); dcTravelService.ResetDCTravelState(); } log.Information("[QuestRotation] Waiting 2s before character switch..."); currentState.Phase = RotationPhase.WaitingBeforeCharacterSwitch; currentState.PhaseStartTime = DateTime.Now; } private void MarkCharacterCompleted(string characterName, string reason = "") { List completedList = currentState.CompletedCharacters; if (!completedList.Contains(characterName)) { completedList.Add(characterName); currentState.CompletedCharacters = completedList; log.Debug("[QuestRotation] Added '" + characterName + "' to completed list" + (string.IsNullOrEmpty(reason) ? "" : (" (" + reason + ")"))); } List remainingList = currentState.RemainingCharacters; bool num = remainingList.Remove(characterName); currentState.RemainingCharacters = remainingList; if (num) { log.Information("[QuestRotation] Removed '" + characterName + "' from remaining list"); log.Information($"[QuestRotation] Progress: {currentState.CompletedCharacters.Count}/{currentState.SelectedCharacters.Count} completed"); } } private void HandleWaitingBeforeCharacterSwitch() { if (movementMonitor != null && movementMonitor.IsMonitoring) { movementMonitor.StopMonitoring(); log.Debug("[QuestRotation] Movement monitor stopped during character switch wait"); } if (condition[ConditionFlag.BetweenAreas]) { log.Debug("[QuestRotation] Character is between areas (Condition 32) - waiting..."); } else if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds >= 2.0) { log.Debug("[QuestRotation] 2s wait complete and not between areas, performing character switch..."); PerformCharacterSwitch(); } } private void PerformCharacterSwitch() { if (currentState.RemainingCharacters.Count == 0) { StopPoint currentStopPoint = stopPoints.FirstOrDefault((StopPoint sp) => sp.IsActive); StopPoint nextStopPoint = stopPoints.FirstOrDefault((StopPoint sp) => !sp.IsActive); if (currentStopPoint != null && nextStopPoint != null) { log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] === CURRENT STOP POINT COMPLETED ==="); log.Information("[QuestRotation] ========================================"); log.Information("[QuestRotation] Completed: " + currentStopPoint.DisplayName); log.Information("[QuestRotation] Moving to next stop point: " + nextStopPoint.DisplayName); currentStopPoint.IsActive = false; nextStopPoint.IsActive = true; currentState.CompletedCharacters.Clear(); currentState.RemainingCharacters = new List(currentState.SelectedCharacters); currentState.CurrentStopQuestId = nextStopPoint.QuestId; string firstChar = currentState.RemainingCharacters[0]; currentState.CurrentCharacter = firstChar; currentState.NextCharacter = firstChar; log.Information("[QuestRotation] Starting next stop point with " + firstChar); if (autoRetainerIpc.SwitchCharacter(firstChar)) { currentState.Phase = RotationPhase.WaitingForCharacterLogin; currentState.PhaseStartTime = DateTime.Now; } else { log.Error("[QuestRotation] Failed to switch to " + firstChar); currentState.Phase = RotationPhase.Error; currentState.ErrorMessage = "Failed to switch to " + firstChar; } } else { log.Information("[QuestRotation] No more stop points to process"); currentState.Phase = RotationPhase.Completed; } return; } if (dcTravelService != null) { dcTravelService.ResetDCTravelState(); log.Information("[QuestRotation] DC Travel state reset for next character"); } log.Debug($"[QuestRotation] DEBUG - RemainingCharacters count: {currentState.RemainingCharacters.Count}"); log.Debug("[QuestRotation] DEBUG - RemainingCharacters list: " + string.Join(", ", currentState.RemainingCharacters)); log.Debug($"[QuestRotation] DEBUG - CompletedCharacters count: {currentState.CompletedCharacters.Count}"); log.Debug("[QuestRotation] DEBUG - CompletedCharacters list: " + string.Join(", ", currentState.CompletedCharacters)); string nextChar = currentState.RemainingCharacters[0]; currentState.CurrentCharacter = nextChar; currentState.NextCharacter = nextChar; log.Information("[QuestRotation] Switching to next character: " + nextChar); log.Information($"[QuestRotation] Progress: {currentState.CompletedCharacters.Count}/{currentState.SelectedCharacters.Count} completed"); if (autoRetainerIpc.SwitchCharacter(nextChar)) { currentState.Phase = RotationPhase.WaitingForCharacterLogin; currentState.PhaseStartTime = DateTime.Now; log.Information("[QuestRotation] Character switch initiated to " + nextChar); } else { log.Error("[QuestRotation] Failed to switch to " + nextChar); currentState.Phase = RotationPhase.Error; currentState.ErrorMessage = "Failed to switch character"; } } public void Dispose() { framework.Update -= OnFrameworkUpdate; log.Information("[QuestRotation] Service disposed"); } }