using System; using System.Collections.Generic; using System.Text.RegularExpressions; using Dalamud.Plugin.Services; using Newtonsoft.Json.Linq; using QuestionableCompanion.Models; namespace QuestionableCompanion.Services; public class AlliedSocietyRotationService : IDisposable { private readonly QuestionableIPC questionableIpc; private readonly AlliedSocietyDatabase database; private readonly AlliedSocietyQuestSelector questSelector; private readonly AutoRetainerIPC autoRetainerIpc; private readonly Configuration configuration; private readonly IPluginLog log; private readonly IFramework framework; private readonly ICommandManager commandManager; private readonly ICondition condition; private readonly IClientState clientState; private bool isRotationActive; private AlliedSocietyRotationPhase currentPhase; private List rotationCharacters = new List(); private int currentCharacterIndex = -1; private string currentCharacterId = string.Empty; private DateTime phaseStartTime = DateTime.MinValue; private int consecutiveNoQuestsCount; private DateTime lastTerritoryTeleportTime = DateTime.MinValue; private string lastTerritoryName = string.Empty; private DateTime lastUpdate = DateTime.MinValue; private const double UpdateIntervalMs = 500.0; private DateTime characterSwitchStartTime = DateTime.MinValue; private const double CharacterSwitchRetrySeconds = 20.0; public bool IsRotationActive => isRotationActive; public string CurrentCharacterId => currentCharacterId; public AlliedSocietyRotationPhase CurrentPhase => currentPhase; public AlliedSocietyRotationService(QuestionableIPC questionableIpc, AlliedSocietyDatabase database, AlliedSocietyQuestSelector questSelector, AutoRetainerIPC autoRetainerIpc, Configuration configuration, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, IClientState clientState) { this.questionableIpc = questionableIpc; this.database = database; this.questSelector = questSelector; this.autoRetainerIpc = autoRetainerIpc; this.configuration = configuration; this.log = log; this.framework = framework; this.commandManager = commandManager; this.condition = condition; this.clientState = clientState; framework.Update += OnFrameworkUpdate; } public void Dispose() { framework.Update -= OnFrameworkUpdate; } public void StartRotation(List characters) { if (isRotationActive) { log.Warning("[AlliedSociety] Rotation already active"); return; } if (characters.Count == 0) { log.Warning("[AlliedSociety] No characters selected for rotation"); return; } log.Information($"[AlliedSociety] Starting rotation with {characters.Count} characters"); rotationCharacters = new List(characters); isRotationActive = true; currentCharacterIndex = -1; AdvanceToNextCharacter(); } public void StopRotation() { if (!isRotationActive) { return; } log.Information("[AlliedSociety] Stopping rotation"); isRotationActive = false; currentPhase = AlliedSocietyRotationPhase.Idle; currentCharacterId = string.Empty; try { commandManager.ProcessCommand("/qst stop"); } catch { } } private void OnFrameworkUpdate(IFramework framework) { if (!isRotationActive || (DateTime.Now - lastUpdate).TotalMilliseconds < 500.0) { return; } lastUpdate = DateTime.Now; try { switch (currentPhase) { case AlliedSocietyRotationPhase.StartingRotation: HandleStartingRotation(); break; case AlliedSocietyRotationPhase.ImportingQuests: HandleImportingQuests(); break; case AlliedSocietyRotationPhase.WaitingForQuestAccept: HandleWaitingForQuestAccept(); break; case AlliedSocietyRotationPhase.MonitoringQuests: HandleMonitoringQuests(); break; case AlliedSocietyRotationPhase.CheckingCompletion: HandleCheckingCompletion(); break; case AlliedSocietyRotationPhase.WaitingForCharacterSwitch: HandleWaitingForCharacterSwitch(); break; } } catch (Exception ex) { log.Error("[AlliedSociety] Error in rotation loop: " + ex.Message); StopRotation(); } } private void HandleStartingRotation() { if (clientState.LocalContentId == 0L) { return; } string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter(); if (string.IsNullOrEmpty(currentLoggedInChar) || currentLoggedInChar != currentCharacterId) { double waitTime = (DateTime.Now - characterSwitchStartTime).TotalSeconds; if (!(waitTime > 20.0)) { return; } log.Warning($"[AlliedSociety] Character switch timeout ({waitTime:F1}s). Retrying /ar relog for {currentCharacterId}..."); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/ays relog " + currentCharacterId); log.Information("[AlliedSociety] Retry relog command sent for " + currentCharacterId); } catch (Exception ex) { log.Error("[AlliedSociety] Failed to send retry relog: " + ex.Message); } }); characterSwitchStartTime = DateTime.Now; } else { questionableIpc.ForceCheckAvailability(); if (questionableIpc.IsAvailable) { log.Information("[AlliedSociety] ✓ Character logged in (" + currentLoggedInChar + "), Questionable ready"); SetPhase(AlliedSocietyRotationPhase.ImportingQuests); } } } private void HandleImportingQuests() { int allowances = questionableIpc.GetAlliedSocietyRemainingAllowances(); log.Information($"[AlliedSociety] Remaining allowances: {allowances}"); if (allowances <= 0) { log.Information("[AlliedSociety] No allowances left. Checking completion..."); SetPhase(AlliedSocietyRotationPhase.CheckingCompletion); return; } List quests = questSelector.SelectQuestsForCharacter(currentCharacterId, allowances, configuration.AlliedSociety.RotationConfig.Priorities, configuration.AlliedSociety.RotationConfig.QuestMode); if (quests.Count == 0) { consecutiveNoQuestsCount++; log.Warning($"[AlliedSociety] No quests selected (attempt {consecutiveNoQuestsCount}/3). Checking completion..."); if (consecutiveNoQuestsCount >= 3) { log.Error("[AlliedSociety] Failed to select quests 3 times consecutively. No quests available for this character."); log.Information("[AlliedSociety] Marking character as complete and moving to next..."); consecutiveNoQuestsCount = 0; database.SetCharacterComplete(currentCharacterId, DateTime.Now); AdvanceToNextCharacter(); } else { SetPhase(AlliedSocietyRotationPhase.CheckingCompletion); } return; } consecutiveNoQuestsCount = 0; log.Information($"[AlliedSociety] Importing {quests.Count} quests to Questionable..."); foreach (string questId in quests) { log.Debug("[AlliedSociety] Adding quest " + questId + " to priority"); questionableIpc.AddQuestPriority(questId); database.AddImportedQuest(currentCharacterId, questId); } log.Information("✓ All quests imported to Questionable"); log.Information("[AlliedSociety] Sending /qst start command..."); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/qst start"); log.Information("[AlliedSociety] ✓ /qst start command sent successfully"); } catch (Exception ex) { log.Error("[AlliedSociety] ✗ Failed to send /qst start: " + ex.Message); } }); log.Information("[AlliedSociety] Transitioning to WaitingForQuestAccept phase"); SetPhase(AlliedSocietyRotationPhase.WaitingForQuestAccept); } private void HandleWaitingForQuestAccept() { try { object task = questionableIpc.GetCurrentTask(); if (task != null && task is JObject jObject) { JToken taskNameToken = jObject["TaskName"]; if (taskNameToken != null) { string taskName = taskNameToken.ToString(); if (!string.IsNullOrEmpty(taskName) && taskName.Contains("Wait(territory:")) { log.Information("[AlliedSociety] [WaitAccept] Territory wait detected: " + taskName); Match territoryMatch = Regex.Match(taskName, "Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)"); if (territoryMatch.Success) { string territoryName = territoryMatch.Groups[1].Value.Trim(); TimeSpan timeSinceLastTeleport = DateTime.Now - lastTerritoryTeleportTime; if (territoryName != lastTerritoryName || timeSinceLastTeleport.TotalSeconds > 10.0) { log.Information("[AlliedSociety] [WaitAccept] ▶ Sending Lifestream teleport: /li " + territoryName); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/li " + territoryName); log.Information("[AlliedSociety] [WaitAccept] ✓ Lifestream command sent successfully"); } catch (Exception ex2) { log.Error("[AlliedSociety] [WaitAccept] ✗ Failed to send Lifestream command: " + ex2.Message); } }); lastTerritoryTeleportTime = DateTime.Now; lastTerritoryName = territoryName; } else { log.Debug("[AlliedSociety] [WaitAccept] Territory teleport debounced for: " + territoryName); } } } } } } catch (Exception ex) { log.Debug("[AlliedSociety] [WaitAccept] Error checking task for teleport: " + ex.Message); } AlliedSocietyCharacterStatus characterStatus = database.GetCharacterStatus(currentCharacterId); bool allAccepted = true; int acceptedCount = 0; int totalCount = characterStatus.ImportedQuestIds.Count; foreach (string questId in characterStatus.ImportedQuestIds) { if (questionableIpc.IsReadyToAcceptQuest(questId)) { allAccepted = false; } else { acceptedCount++; } } if (allAccepted) { log.Information($"[AlliedSociety] ✓ All {totalCount} quests accepted. Monitoring progress..."); SetPhase(AlliedSocietyRotationPhase.MonitoringQuests); } } private void HandleMonitoringQuests() { string currentQuestId = questionableIpc.GetCurrentQuestId(); AlliedSocietyCharacterStatus status = database.GetCharacterStatus(currentCharacterId); log.Debug("[AlliedSociety] Monitoring - Current Quest: " + (currentQuestId ?? "null")); if (currentQuestId != null && status.ImportedQuestIds.Contains(currentQuestId)) { log.Debug("[AlliedSociety] Working on imported quest: " + currentQuestId); try { object task = questionableIpc.GetCurrentTask(); log.Debug("[AlliedSociety] Task object type: " + (task?.GetType().Name ?? "null")); if (task != null && task is JObject jObject) { log.Debug("[AlliedSociety] Task JObject: " + jObject.ToString()); JToken taskNameToken = jObject["TaskName"]; if (taskNameToken != null) { string taskName = taskNameToken.ToString(); log.Information("[AlliedSociety] Current Task Name: '" + taskName + "'"); if (!string.IsNullOrEmpty(taskName) && taskName.Contains("Wait(territory:")) { log.Information("[AlliedSociety] ✓ TERRITORY WAIT DETECTED! Raw task: " + taskName); Match territoryMatch = Regex.Match(taskName, "Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)"); if (territoryMatch.Success) { string territoryName = territoryMatch.Groups[1].Value.Trim(); log.Information("[AlliedSociety] ✓ Territory name parsed: '" + territoryName + "'"); TimeSpan timeSinceLastTeleport = DateTime.Now - lastTerritoryTeleportTime; if (territoryName != lastTerritoryName || timeSinceLastTeleport.TotalSeconds > 10.0) { log.Information("[AlliedSociety] ▶ Territory wait detected: " + territoryName); log.Information("[AlliedSociety] ▶ Sending Lifestream teleport command: /li " + territoryName); framework.RunOnFrameworkThread(delegate { try { commandManager.ProcessCommand("/li " + territoryName); log.Information("[AlliedSociety] ✓ Lifestream command sent successfully"); } catch (Exception ex2) { log.Error("[AlliedSociety] ✗ Failed to send Lifestream command: " + ex2.Message); } }); lastTerritoryTeleportTime = DateTime.Now; lastTerritoryName = territoryName; } else { log.Information($"[AlliedSociety] ⏸ Territory teleport debounced for: {territoryName} (last: {timeSinceLastTeleport.TotalSeconds:F1}s ago)"); } } else { log.Warning("[AlliedSociety] ✗ Territory wait detected but regex didn't match! Raw: " + taskName); } } else if (!string.IsNullOrEmpty(taskName) && taskName.ToLower().Contains("wait")) { log.Debug("[AlliedSociety] Wait task (not territory): " + taskName); } } else { log.Debug("[AlliedSociety] Task has no TaskName property"); } } else if (task == null) { log.Debug("[AlliedSociety] GetCurrentTask returned null"); } else { log.Warning("[AlliedSociety] Task is not JObject, type: " + task.GetType().Name); } return; } catch (Exception ex) { log.Error("[AlliedSociety] Error checking task for teleport: " + ex.Message + "\n" + ex.StackTrace); return; } } if (currentQuestId == null || !status.ImportedQuestIds.Contains(currentQuestId)) { log.Information("[AlliedSociety] No longer working on imported quests. Checking completion..."); SetPhase(AlliedSocietyRotationPhase.CheckingCompletion); } } private void HandleCheckingCompletion() { int allowances = questionableIpc.GetAlliedSocietyRemainingAllowances(); log.Information($"[AlliedSociety] Checking completion. Allowances: {allowances}"); if (allowances == 0) { string currentQuestId = questionableIpc.GetCurrentQuestId(); AlliedSocietyCharacterStatus status = database.GetCharacterStatus(currentCharacterId); if (currentQuestId != null && status.ImportedQuestIds.Contains(currentQuestId)) { log.Information("[AlliedSociety] Still working on final quest " + currentQuestId + ". Waiting..."); return; } log.Information("[AlliedSociety] Character " + currentCharacterId + " completed all allowances."); try { commandManager.ProcessCommand("/qst stop"); log.Information("[AlliedSociety] Sent /qst stop command after quest completion"); } catch (Exception ex) { log.Error("[AlliedSociety] Failed to send /qst stop: " + ex.Message); } questionableIpc.ClearQuestPriority(); database.SetCharacterComplete(currentCharacterId, DateTime.Now); SetPhase(AlliedSocietyRotationPhase.WaitingForCharacterSwitch); } else { log.Information("[AlliedSociety] Allowances remaining. Trying to import more quests..."); questionableIpc.ClearQuestPriority(); database.ClearImportedQuests(currentCharacterId); SetPhase(AlliedSocietyRotationPhase.ImportingQuests); } } private void HandleWaitingForCharacterSwitch() { if (!((DateTime.Now - phaseStartTime).TotalSeconds < 2.0)) { AdvanceToNextCharacter(); } } private void AdvanceToNextCharacter() { currentCharacterIndex++; if (currentCharacterIndex >= rotationCharacters.Count) { log.Information("[AlliedSociety] Rotation completed for all characters."); StopRotation(); return; } string nextChar = rotationCharacters[currentCharacterIndex]; if (database.GetCharacterStatus(nextChar).Status == AlliedSocietyRotationStatus.Complete) { log.Information("[AlliedSociety] Skipping " + nextChar + " (Already Complete)"); AdvanceToNextCharacter(); return; } log.Information("[AlliedSociety] Switching to " + nextChar); currentCharacterId = nextChar; characterSwitchStartTime = DateTime.Now; if (autoRetainerIpc.SwitchCharacter(nextChar)) { SetPhase(AlliedSocietyRotationPhase.StartingRotation); return; } log.Error("[AlliedSociety] Failed to switch to " + nextChar); StopRotation(); } private void SetPhase(AlliedSocietyRotationPhase phase) { log.Information($"[AlliedSociety] Phase: {currentPhase} → {phase}"); currentPhase = phase; phaseStartTime = DateTime.Now; } }