qstbak/QuestionableCompanion/QuestionableCompanion.Services/AlliedSocietyRotationService.cs
2025-12-04 04:39:08 +10:00

496 lines
16 KiB
C#

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<string> rotationCharacters = new List<string>();
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<string> 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<string>(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<string> 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;
}
}