qstbak/QuestionableCompanion/QuestionableCompanion.Services/QuestRotationExecutionService.cs
2025-12-04 04:40:03 +10:00

1427 lines
48 KiB
C#

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 readonly List<StopPoint> stopPoints = new List<StopPoint>();
private RotationState currentState = new RotationState();
private Dictionary<uint, List<string>> questCompletionByCharacter = new Dictionary<uint, List<string>>();
private Action? onDataChanged;
private DateTime lastCheckTime = DateTime.MinValue;
private const double CheckIntervalMs = 250.0;
private DateTime lastSubmarineCheckTime = DateTime.MinValue;
private bool waitingForQuestAcceptForSubmarines;
private uint? lastDungeonQuestId;
private int? lastDungeonSequence;
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<string> stopQuestIds = questionableIPC.GetStopQuestList();
Dictionary<string, object> 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<string, object> 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<string> 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<string> remainingChars = new List<string>();
List<string> completedChars = new List<string>();
if (questCompletionByCharacter.TryGetValue(questId, out List<string> 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<string>(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<string> 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<string> remainingChars = new List<string>(characters);
currentState = new RotationState
{
CurrentStopQuestId = 0u,
SelectedCharacters = new List<string>(characters),
RemainingCharacters = remainingChars,
CompletedCharacters = new List<string>(),
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<string> characters)
{
if (characters == null || characters.Count == 0)
{
log.Error("[QuestRotation] Cannot start sync rotation: No characters provided");
return false;
}
List<string> charactersToSync = new List<string>();
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<string>(charactersToSync),
RemainingCharacters = new List<string>(charactersToSync),
CompletedCharacters = new List<string>(),
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<string> 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<StopPoint> GetAllStopPoints()
{
return new List<StopPoint>(stopPoints);
}
public void LoadQuestCompletionData(Dictionary<uint, List<string>> data)
{
if (data != null && data.Count > 0)
{
questCompletionByCharacter = new Dictionary<uint, List<string>>(data);
log.Information($"[QuestRotation] Loaded quest completion data for {data.Count} quests");
int count = 0;
foreach (KeyValuePair<uint, List<string>> 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<uint, List<string>> GetQuestCompletionData()
{
return new Dictionary<uint, List<string>>(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");
}
private void MarkQuestCompleted(uint questId, string characterName)
{
if (!questCompletionByCharacter.ContainsKey(questId))
{
questCompletionByCharacter[questId] = new List<string>();
}
if (!questCompletionByCharacter[questId].Contains(characterName))
{
questCompletionByCharacter[questId].Add(characterName);
log.Debug($"[QuestRotation] Marked {characterName} as completed quest {questId}");
onDataChanged?.Invoke();
}
}
public List<uint> GetCompletedQuestsByCharacter(string characterName)
{
List<uint> completedQuests = new List<uint>();
foreach (KeyValuePair<uint, List<string>> 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<string> characters) && characters.Contains(characterName))
{
return true;
}
if (preCheckService != null)
{
bool? status = preCheckService.GetQuestStatus(characterName, questId);
if (status.HasValue && status.Value)
{
return true;
}
}
return false;
}
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 (deathHandler != null && combatDutyDetection != null && !combatDutyDetection.IsInDuty)
{
deathHandler.Update();
}
if (dungeonAutomation != null)
{
if (submarineManager.IsSubmarinePaused)
{
log.Debug("[QuestRotation] Submarine multi-mode active - skipping dungeon validation");
}
else
{
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)
{
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;
if (!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 currentQuestIdStr = questionableIPC.GetCurrentQuestId();
byte? currentSequence = questionableIPC.GetCurrentSequence();
if (!string.IsNullOrEmpty(currentQuestIdStr) && currentSequence.HasValue && uint.TryParse(currentQuestIdStr, out var currentQuestId))
{
if (currentQuestId == questId)
{
if (currentSequence.Value > activeStopPoint.Sequence.Value)
{
log.Information($"[QuestRotation] ✓ Quest {questId} Sequence {activeStopPoint.Sequence.Value} completed by {currentState.CurrentCharacter}!");
log.Information($"[QuestRotation] Current Sequence: {currentSequence.Value} (moved past {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<string> completed = currentState.CompletedCharacters;
List<string> 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;
}
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);
}
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<string> 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<string> 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 && configuration.EnableMultiModeAfterRotation)
{
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<string>(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 (Multi-Mode: " + (configuration.EnableMultiModeAfterRotation ? "Enabled" : "Disabled") + ")");
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");
}
}