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

609 lines
19 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Dalamud.Game.ClientState.Conditions;
using Dalamud.Plugin.Services;
using FFXIVClientStructs.FFXIV.Client.Game;
using Newtonsoft.Json.Linq;
namespace QuestionableCompanion.Services;
public class EventQuestExecutionService : IDisposable
{
private readonly AutoRetainerIPC autoRetainerIpc;
private readonly QuestionableIPC questionableIPC;
private readonly IPluginLog log;
private readonly IFramework framework;
private readonly ICommandManager commandManager;
private readonly ICondition condition;
private readonly Configuration configuration;
private readonly EventQuestResolver eventQuestResolver;
private EventQuestState currentState = new EventQuestState();
private Dictionary<string, List<string>> eventQuestCompletionByCharacter = new Dictionary<string, List<string>>();
private DateTime lastCheckTime = DateTime.MinValue;
private const double CheckIntervalMs = 250.0;
private bool isRotationActive;
private string? lastTerritoryWaitDetected;
private DateTime lastTerritoryTeleportTime = DateTime.MinValue;
private Action? onDataChanged;
public bool IsRotationActive => isRotationActive;
public EventQuestExecutionService(AutoRetainerIPC autoRetainerIpc, QuestionableIPC questionableIPC, IPluginLog log, IFramework framework, ICommandManager commandManager, ICondition condition, Configuration configuration, IDataManager dataManager, Action? onDataChanged = null)
{
this.autoRetainerIpc = autoRetainerIpc;
this.questionableIPC = questionableIPC;
this.log = log;
this.framework = framework;
this.commandManager = commandManager;
this.condition = condition;
this.configuration = configuration;
this.onDataChanged = onDataChanged;
eventQuestResolver = new EventQuestResolver(dataManager, log);
framework.Update += OnFrameworkUpdate;
log.Information("[EventQuest] Service initialized");
}
public bool StartEventQuestRotation(string eventQuestId, List<string> characters)
{
if (characters == null || characters.Count == 0)
{
log.Error("[EventQuest] Cannot start rotation: No characters selected");
return false;
}
if (string.IsNullOrEmpty(eventQuestId))
{
log.Error("[EventQuest] Cannot start rotation: Event Quest ID is empty");
return false;
}
List<string> dependencies = eventQuestResolver.ResolveEventQuestDependencies(eventQuestId);
List<string> remainingChars = new List<string>();
List<string> completedChars = new List<string>();
foreach (string character in characters)
{
if (HasCharacterCompletedEventQuest(eventQuestId, character))
{
completedChars.Add(character);
log.Debug("[EventQuest] " + character + " already completed event quest " + eventQuestId);
}
else
{
remainingChars.Add(character);
log.Debug("[EventQuest] " + character + " needs to complete event quest " + eventQuestId);
}
}
if (remainingChars.Count == 0)
{
log.Information("[EventQuest] All characters have already completed event quest " + eventQuestId);
return false;
}
string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter();
bool isAlreadyLoggedIn = !string.IsNullOrEmpty(currentLoggedInChar) && remainingChars.Contains(currentLoggedInChar);
currentState = new EventQuestState
{
EventQuestId = eventQuestId,
EventQuestName = eventQuestResolver.GetQuestName(eventQuestId),
SelectedCharacters = new List<string>(characters),
RemainingCharacters = remainingChars,
CompletedCharacters = completedChars,
DependencyQuests = dependencies,
Phase = ((!isAlreadyLoggedIn) ? EventQuestPhase.InitializingFirstCharacter : EventQuestPhase.CheckingQuestCompletion),
CurrentCharacter = (isAlreadyLoggedIn ? currentLoggedInChar : ""),
PhaseStartTime = DateTime.Now,
RotationStartTime = DateTime.Now
};
isRotationActive = true;
log.Information("[EventQuest] ═══ Starting Event Quest Rotation ═══");
log.Information($"[EventQuest] Event Quest: {currentState.EventQuestName} ({eventQuestId})");
log.Information($"[EventQuest] Total Characters: {characters.Count}");
log.Information($"[EventQuest] Remaining: {remainingChars.Count} | Completed: {completedChars.Count}");
log.Information($"[EventQuest] Dependencies to resolve: {dependencies.Count}");
if (dependencies.Count > 0)
{
log.Information("[EventQuest] Prerequisites: " + string.Join(", ", dependencies.Select((string id) => eventQuestResolver.GetQuestName(id))));
}
if (isAlreadyLoggedIn)
{
log.Information("[EventQuest] User already logged in as " + currentLoggedInChar + " - starting immediately");
}
return true;
}
public EventQuestState GetCurrentState()
{
return currentState;
}
public void LoadEventQuestCompletionData(Dictionary<string, List<string>> data)
{
if (data != null && data.Count > 0)
{
eventQuestCompletionByCharacter = new Dictionary<string, List<string>>(data);
log.Information($"[EventQuest] Loaded completion data for {data.Count} event quests");
}
}
public Dictionary<string, List<string>> GetEventQuestCompletionData()
{
return new Dictionary<string, List<string>>(eventQuestCompletionByCharacter);
}
public void AbortRotation()
{
log.Information("[EventQuest] Aborting Event Quest rotation");
currentState = new EventQuestState
{
Phase = EventQuestPhase.Idle
};
isRotationActive = false;
}
private void MarkEventQuestCompleted(string eventQuestId, string characterName)
{
if (!eventQuestCompletionByCharacter.ContainsKey(eventQuestId))
{
eventQuestCompletionByCharacter[eventQuestId] = new List<string>();
}
if (!eventQuestCompletionByCharacter[eventQuestId].Contains(characterName))
{
eventQuestCompletionByCharacter[eventQuestId].Add(characterName);
log.Debug("[EventQuest] Marked " + characterName + " as completed event quest " + eventQuestId);
onDataChanged?.Invoke();
}
}
private bool HasCharacterCompletedEventQuest(string eventQuestId, string characterName)
{
if (eventQuestCompletionByCharacter.TryGetValue(eventQuestId, out List<string> characters))
{
return characters.Contains(characterName);
}
return false;
}
private void OnFrameworkUpdate(IFramework framework)
{
if (!isRotationActive)
{
return;
}
DateTime now = DateTime.Now;
if (!((now - lastCheckTime).TotalMilliseconds < 250.0))
{
lastCheckTime = now;
CheckForTerritoryWait();
switch (currentState.Phase)
{
case EventQuestPhase.InitializingFirstCharacter:
HandleInitializingFirstCharacter();
break;
case EventQuestPhase.WaitingForCharacterLogin:
HandleWaitingForCharacterLogin();
break;
case EventQuestPhase.CheckingQuestCompletion:
HandleCheckingQuestCompletion();
break;
case EventQuestPhase.ResolvingDependencies:
HandleResolvingDependencies();
break;
case EventQuestPhase.ExecutingDependencies:
HandleExecutingDependencies();
break;
case EventQuestPhase.WaitingForQuestStart:
case EventQuestPhase.QuestActive:
HandleQuestMonitoring();
break;
case EventQuestPhase.WaitingBeforeCharacterSwitch:
HandleWaitingBeforeCharacterSwitch();
break;
case EventQuestPhase.Completed:
HandleCompleted();
break;
}
}
}
private void CheckForTerritoryWait()
{
if (!questionableIPC.IsRunning())
{
return;
}
object task = questionableIPC.GetCurrentTask();
if (task == null)
{
return;
}
try
{
if (!(task is JObject jObject))
{
return;
}
JToken taskNameToken = jObject["TaskName"];
if (taskNameToken == null)
{
return;
}
string taskName = taskNameToken.ToString();
if (string.IsNullOrEmpty(taskName))
{
return;
}
Match waitTerritoryMatch = new Regex("Wait\\(territory:\\s*(.+?)\\s*\\((\\d+)\\)\\)").Match(taskName);
if (!waitTerritoryMatch.Success)
{
return;
}
string territoryName = waitTerritoryMatch.Groups[1].Value.Trim();
uint territoryId = uint.Parse(waitTerritoryMatch.Groups[2].Value);
string territoryKey = $"{territoryName}_{territoryId}";
double timeSinceLastTeleport = (DateTime.Now - lastTerritoryTeleportTime).TotalSeconds;
if (lastTerritoryWaitDetected == territoryKey && timeSinceLastTeleport < 60.0)
{
return;
}
log.Information($"[EventQuest] Wait(territory) detected: {territoryName} (ID: {territoryId})");
log.Information("[EventQuest] Auto-teleporting via Lifestream...");
lastTerritoryWaitDetected = territoryKey;
lastTerritoryTeleportTime = DateTime.Now;
framework.RunOnFrameworkThread(delegate
{
try
{
string text = "/li " + territoryName;
commandManager.ProcessCommand(text);
log.Information("[EventQuest] Sent teleport command: " + text);
}
catch (Exception ex2)
{
log.Error("[EventQuest] Failed to teleport to " + territoryName + ": " + ex2.Message);
}
});
}
catch (Exception ex)
{
log.Error("[EventQuest] Error checking Wait(territory) task: " + ex.Message);
}
}
private void HandleInitializingFirstCharacter()
{
if (currentState.RemainingCharacters.Count == 0)
{
log.Information("[EventQuest] No remaining characters - rotation complete");
currentState.Phase = EventQuestPhase.Completed;
isRotationActive = false;
return;
}
string firstChar = currentState.RemainingCharacters[0];
currentState.CurrentCharacter = firstChar;
log.Information("[EventQuest] >>> Initializing first character: " + firstChar);
if (autoRetainerIpc.SwitchCharacter(firstChar))
{
currentState.Phase = EventQuestPhase.WaitingForCharacterLogin;
currentState.PhaseStartTime = DateTime.Now;
log.Information("[EventQuest] Character switch initiated to " + firstChar);
}
else
{
log.Error("[EventQuest] Failed to switch to " + firstChar);
currentState.Phase = EventQuestPhase.Error;
currentState.ErrorMessage = "Failed to switch to " + firstChar;
}
}
private void HandleWaitingForCharacterLogin()
{
if ((DateTime.Now - currentState.PhaseStartTime).TotalSeconds > 60.0)
{
log.Error("[EventQuest] Login timeout for " + currentState.CurrentCharacter);
SkipToNextCharacter();
return;
}
string currentLoggedInChar = autoRetainerIpc.GetCurrentCharacter();
if (!string.IsNullOrEmpty(currentLoggedInChar) && currentLoggedInChar == currentState.CurrentCharacter && !((DateTime.Now - currentState.PhaseStartTime).TotalSeconds < 5.0))
{
log.Information("[EventQuest] Successfully logged in as " + currentLoggedInChar);
currentState.Phase = EventQuestPhase.CheckingQuestCompletion;
currentState.PhaseStartTime = DateTime.Now;
}
}
private void HandleCheckingQuestCompletion()
{
string eventQuestId = currentState.EventQuestId;
string rawId = QuestIdParser.ParseQuestId(eventQuestId).rawId;
QuestIdType questType = QuestIdParser.ClassifyQuestId(eventQuestId);
log.Debug($"[EventQuest] Checking completion for {eventQuestId} (Type: {questType}, RawId: {rawId})");
if (!uint.TryParse(rawId, out var questIdUint))
{
log.Error($"[EventQuest] Invalid quest ID: {eventQuestId} (cannot parse numeric part: {rawId})");
SkipToNextCharacter();
return;
}
bool isQuestComplete = false;
try
{
isQuestComplete = QuestManager.IsQuestComplete(questIdUint);
}
catch (Exception ex)
{
log.Error("[EventQuest] Error checking quest completion: " + ex.Message);
}
if (isQuestComplete)
{
log.Information("[EventQuest] " + currentState.CurrentCharacter + " already completed event quest " + eventQuestId);
List<string> completedList = currentState.CompletedCharacters;
if (!completedList.Contains(currentState.CurrentCharacter))
{
completedList.Add(currentState.CurrentCharacter);
currentState.CompletedCharacters = completedList;
}
MarkEventQuestCompleted(eventQuestId, currentState.CurrentCharacter);
SkipToNextCharacter();
}
else
{
log.Information("[EventQuest] " + currentState.CurrentCharacter + " needs to complete event quest " + eventQuestId);
log.Information($"[EventQuest] >>> Starting event quest with {currentState.DependencyQuests.Count} prerequisites");
StartEventQuest();
}
}
private void HandleResolvingDependencies()
{
log.Information("[EventQuest] All prerequisites completed - starting event quest");
StartEventQuest();
}
private void HandleExecutingDependencies()
{
string depQuestId = currentState.CurrentExecutingQuest;
if (!uint.TryParse(depQuestId, out var questIdUint))
{
log.Error("[EventQuest] Invalid dependency quest ID: " + depQuestId);
currentState.DependencyIndex++;
currentState.Phase = EventQuestPhase.ResolvingDependencies;
return;
}
bool isDependencyComplete = false;
try
{
isDependencyComplete = QuestManager.IsQuestComplete(questIdUint);
}
catch
{
}
if (isDependencyComplete)
{
log.Information("[EventQuest] Dependency " + eventQuestResolver.GetQuestName(depQuestId) + " already completed");
currentState.DependencyIndex++;
currentState.Phase = EventQuestPhase.ResolvingDependencies;
return;
}
try
{
commandManager.ProcessCommand("/qst start");
log.Information("[EventQuest] Started dependency quest: " + eventQuestResolver.GetQuestName(depQuestId));
}
catch (Exception ex)
{
log.Error("[EventQuest] Failed to start dependency: " + ex.Message);
}
currentState.Phase = EventQuestPhase.QuestActive;
currentState.HasEventQuestBeenAccepted = false;
currentState.PhaseStartTime = DateTime.Now;
}
private void HandleQuestMonitoring()
{
string eventQuestId = currentState.EventQuestId;
try
{
if (questionableIPC.IsQuestComplete(eventQuestId))
{
log.Information("[EventQuest] Event quest " + eventQuestId + " completed by " + currentState.CurrentCharacter);
MarkEventQuestCompleted(currentState.EventQuestId, currentState.CurrentCharacter);
List<string> completedList = currentState.CompletedCharacters;
if (!completedList.Contains(currentState.CurrentCharacter))
{
completedList.Add(currentState.CurrentCharacter);
currentState.CompletedCharacters = completedList;
}
try
{
commandManager.ProcessCommand("/qst stop");
log.Information("[EventQuest] Sent /qst stop");
}
catch
{
}
currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch;
currentState.PhaseStartTime = DateTime.Now;
}
}
catch (Exception ex)
{
log.Error("[EventQuest] Error checking quest completion via IPC: " + ex.Message);
}
}
private void HandleWaitingBeforeCharacterSwitch()
{
if (!condition[ConditionFlag.BetweenAreas] && (DateTime.Now - currentState.PhaseStartTime).TotalSeconds >= 2.0)
{
PerformCharacterSwitch();
}
}
private void HandleCompleted()
{
log.Information("[EventQuest] ═══ EVENT QUEST ROTATION COMPLETED ═══");
log.Information($"[EventQuest] All {currentState.CompletedCharacters.Count} characters completed the event quest");
if (questionableIPC.IsAvailable)
{
try
{
questionableIPC.ClearQuestPriority();
log.Information("[EventQuest] Cleared quest priority queue after completion");
}
catch (Exception ex)
{
log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message);
}
}
isRotationActive = false;
currentState.Phase = EventQuestPhase.Idle;
}
private void StartEventQuest()
{
List<string> allQuests = new List<string>();
if (currentState.DependencyQuests.Count > 0)
{
foreach (string dep in currentState.DependencyQuests)
{
allQuests.Add(dep);
QuestIdType questType = QuestIdParser.ClassifyQuestId(dep);
log.Information($"[EventQuest] Adding dependency: {dep} (Type: {questType})");
}
}
string mainQuestId = currentState.EventQuestId;
allQuests.Add(mainQuestId);
QuestIdType mainQuestType = QuestIdParser.ClassifyQuestId(mainQuestId);
log.Information($"[EventQuest] Adding main event quest: {mainQuestId} (Type: {mainQuestType})");
log.Information($"[EventQuest] Setting {allQuests.Count} quests as Questionable priority");
if (questionableIPC.IsAvailable)
{
try
{
questionableIPC.ClearQuestPriority();
log.Information("[EventQuest] Cleared existing quest priority queue");
}
catch (Exception ex)
{
log.Warning("[EventQuest] Failed to clear quest priority: " + ex.Message);
}
foreach (string questId in allQuests)
{
try
{
bool result = questionableIPC.AddQuestPriority(questId);
log.Information($"[EventQuest] Added quest {questId} to priority: {result}");
}
catch (Exception ex2)
{
log.Warning("[EventQuest] Failed to add quest " + questId + " to priority: " + ex2.Message);
}
}
}
else
{
log.Warning("[EventQuest] Questionable IPC not available - cannot set priority");
}
if (condition[ConditionFlag.BetweenAreas])
{
log.Debug("[EventQuest] Character is between areas - waiting before starting quest");
return;
}
if (questionableIPC.IsAvailable && questionableIPC.IsRunning())
{
log.Debug("[EventQuest] Questionable is busy - waiting before starting quest");
return;
}
try
{
commandManager.ProcessCommand("/qst start");
log.Information("[EventQuest] Sent /qst start for event quest");
currentState.Phase = EventQuestPhase.QuestActive;
currentState.CurrentExecutingQuest = currentState.EventQuestId;
currentState.HasEventQuestBeenAccepted = false;
currentState.PhaseStartTime = DateTime.Now;
}
catch (Exception ex3)
{
log.Error("[EventQuest] Failed to start quest: " + ex3.Message);
}
}
private void SkipToNextCharacter()
{
try
{
commandManager.ProcessCommand("/qst stop");
log.Information("[EventQuest] Sent /qst stop before character switch");
}
catch
{
}
List<string> remainingList = currentState.RemainingCharacters;
List<string> completedList = currentState.CompletedCharacters;
if (remainingList.Contains(currentState.CurrentCharacter))
{
remainingList.Remove(currentState.CurrentCharacter);
currentState.RemainingCharacters = remainingList;
}
if (!completedList.Contains(currentState.CurrentCharacter))
{
completedList.Add(currentState.CurrentCharacter);
currentState.CompletedCharacters = completedList;
log.Information("[EventQuest] Character " + currentState.CurrentCharacter + " marked as completed (skipped)");
}
currentState.Phase = EventQuestPhase.WaitingBeforeCharacterSwitch;
currentState.PhaseStartTime = DateTime.Now;
}
private void PerformCharacterSwitch()
{
List<string> remainingList = currentState.RemainingCharacters;
if (remainingList.Contains(currentState.CurrentCharacter))
{
remainingList.Remove(currentState.CurrentCharacter);
currentState.RemainingCharacters = remainingList;
}
if (currentState.RemainingCharacters.Count == 0)
{
currentState.Phase = EventQuestPhase.Completed;
return;
}
string nextChar = currentState.RemainingCharacters[0];
currentState.CurrentCharacter = nextChar;
currentState.NextCharacter = nextChar;
log.Information("[EventQuest] Switching to next character: " + nextChar);
log.Information($"[EventQuest] Progress: {currentState.CompletedCharacters.Count}/{currentState.SelectedCharacters.Count} completed");
if (autoRetainerIpc.SwitchCharacter(nextChar))
{
currentState.Phase = EventQuestPhase.WaitingForCharacterLogin;
currentState.PhaseStartTime = DateTime.Now;
}
else
{
log.Error("[EventQuest] Failed to switch to " + nextChar);
currentState.Phase = EventQuestPhase.Error;
currentState.ErrorMessage = "Failed to switch character";
}
}
public void Dispose()
{
framework.Update -= OnFrameworkUpdate;
log.Information("[EventQuest] Service disposed");
}
}