496 lines
16 KiB
C#
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;
|
|
}
|
|
}
|