using System; using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.Text; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.UI; namespace QuestionableCompanion.Services; public class ARRTrialAutomationService : IDisposable { private readonly IPluginLog log; private readonly IFramework framework; private readonly ICommandManager commandManager; private readonly IChatGui chatGui; private readonly Configuration config; private readonly QuestionableIPC questionableIPC; private readonly SubmarineManager submarineManager; private readonly HelperManager helperManager; private readonly IPartyList partyList; private readonly ICondition condition; private readonly MemoryHelper memoryHelper; private bool isInDuty; private static readonly (uint QuestId, uint TrialId, string ADCommand, string Name)[] Trials = new(uint, uint, string, string)[3] { (1048u, 20004u, "/ad run trial 292 1", "Ifrit HM"), (1157u, 20006u, "/ad run trial 294 1", "Garuda HM"), (1158u, 20005u, "/ad run trial 293 1", "Titan HM") }; private const uint TRIGGER_QUEST = 89u; private const uint TARGET_QUEST = 363u; private bool isProcessing; private int currentTrialIndex = -1; private bool waitingForQuest; private bool waitingForParty; private bool waitingForTrial; private DateTime lastCheckTime = DateTime.MinValue; public ARRTrialAutomationService(IPluginLog log, IFramework framework, ICommandManager commandManager, IChatGui chatGui, Configuration config, QuestionableIPC questionableIPC, SubmarineManager submarineManager, HelperManager helperManager, IPartyList partyList, ICondition condition, MemoryHelper memoryHelper) { this.log = log; this.framework = framework; this.commandManager = commandManager; this.chatGui = chatGui; this.config = config; this.questionableIPC = questionableIPC; this.submarineManager = submarineManager; this.helperManager = helperManager; this.partyList = partyList; this.condition = condition; this.memoryHelper = memoryHelper; framework.Update += OnFrameworkUpdate; condition.ConditionChange += OnConditionChanged; log.Information("[ARRTrials] Service initialized"); } private void OnFrameworkUpdate(IFramework framework) { if (!isProcessing) { return; } if (waitingForParty && partyList != null && partyList.Length > 1) { if (!((DateTime.Now - lastCheckTime).TotalSeconds < 1.0)) { lastCheckTime = DateTime.Now; log.Information($"[ARRTrials] Party join detected (Size: {partyList.Length}) - Triggering trial..."); waitingForParty = false; TriggerCurrentTrial(); } } else if (waitingForQuest && currentTrialIndex >= 0 && currentTrialIndex < Trials.Length && !((DateTime.Now - lastCheckTime).TotalSeconds < 2.0)) { lastCheckTime = DateTime.Now; (uint QuestId, uint TrialId, string ADCommand, string Name) tuple = Trials[currentTrialIndex]; uint trialId = tuple.TrialId; string name = tuple.Name; bool unlocked = IsTrialUnlocked(trialId); log.Debug($"[ARRTrials] Polling {name} ({trialId}) Unlocked: {unlocked}"); if (unlocked) { log.Information("[ARRTrials] Polling detected " + name + " unlocked - Proceeding..."); waitingForQuest = false; helperManager.InviteHelpers(); waitingForParty = true; } } } public bool IsTrialComplete(uint instanceId) { return UIState.IsInstanceContentCompleted(instanceId); } public bool IsTrialUnlocked(uint instanceId) { return UIState.IsInstanceContentUnlocked(instanceId); } public bool IsTargetQuestAvailableOrComplete() { if (QuestManager.IsQuestComplete(363u)) { return true; } if (questionableIPC.IsReadyToAcceptQuest(363u.ToString())) { return true; } return false; } public void OnTriggerQuestComplete() { if (!config.EnableARRPrimalCheck) { log.Debug("[ARRTrials] Feature disabled, skipping check"); return; } log.Information("[ARRTrials] Quest 89 complete, starting ARR Primal check..."); StartTrialChain(); } public void StartTrialChain() { if (isProcessing) { log.Debug("[ARRTrials] Already processing trial chain"); return; } isProcessing = true; submarineManager.SetExternalPause(paused: true); int startIndex = -1; for (int i = Trials.Length - 1; i >= 0; i--) { if (!IsTrialComplete(Trials[i].TrialId)) { for (int j = 0; j <= i; j++) { if (!IsTrialComplete(Trials[j].TrialId)) { startIndex = j; break; } } break; } } if (startIndex == -1) { log.Information("[ARRTrials] All trials already complete!"); isProcessing = false; submarineManager.SetExternalPause(paused: false); return; } currentTrialIndex = startIndex; log.Information($"[ARRTrials] Starting from trial index {startIndex}: {Trials[startIndex].Name}"); ProcessCurrentTrial(); } private void ProcessCurrentTrial() { if (currentTrialIndex < 0 || currentTrialIndex >= Trials.Length) { log.Information("[ARRTrials] Trial chain complete!"); isProcessing = false; submarineManager.SetExternalPause(paused: false); return; } var (questId, trialId, _, name) = Trials[currentTrialIndex]; if (IsTrialComplete(trialId)) { log.Information("[ARRTrials] " + name + " already complete, moving to next"); currentTrialIndex++; ProcessCurrentTrial(); } else if (!QuestManager.IsQuestComplete(questId)) { log.Information($"[ARRTrials] Queueing unlock quest {questId} for {name}"); questionableIPC.AddQuestPriority(questId.ToString()); framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/qst start"); }); waitingForQuest = true; } else { log.Information("[ARRTrials] " + name + " unlocked, inviting helper and triggering trial..."); helperManager.InviteHelpers(); waitingForParty = true; } } public void OnQuestComplete(uint questId) { if (!isProcessing || !waitingForQuest) { return; } for (int i = currentTrialIndex; i < Trials.Length; i++) { if (Trials[i].QuestId == questId) { log.Information($"[ARRTrials] Unlock quest {questId} completed, triggering trial"); waitingForQuest = false; helperManager.InviteHelpers(); waitingForParty = true; break; } } } public void OnPartyReady() { if (isProcessing && waitingForParty) { waitingForParty = false; TriggerCurrentTrial(); } } private void TriggerCurrentTrial() { if (currentTrialIndex >= 0 && currentTrialIndex < Trials.Length) { (uint, uint, string, string) tuple = Trials[currentTrialIndex]; string adCommand = tuple.Item3; string name = tuple.Item4; log.Information("[ARRTrials] Triggering " + name + " via AD command"); framework.RunOnFrameworkThread(delegate { chatGui.Print(new XivChatEntry { Message = "[QSTCompanion] Triggering " + name + "...", Type = XivChatType.Echo }); commandManager.ProcessCommand("/ad cfg Unsynced true"); commandManager.ProcessCommand(adCommand); }); waitingForTrial = true; } } public void OnDutyComplete() { if (!isProcessing || !waitingForTrial) { return; } (uint QuestId, uint TrialId, string ADCommand, string Name) tuple = Trials[currentTrialIndex]; uint trialId = tuple.TrialId; string name = tuple.Name; if (IsTrialComplete(trialId)) { log.Information("[ARRTrials] " + name + " completed successfully!"); waitingForTrial = false; currentTrialIndex++; framework.RunOnFrameworkThread(delegate { ProcessCurrentTrial(); }); } else { log.Warning("[ARRTrials] " + name + " NOT complete after verification. Retrying current step..."); waitingForTrial = false; framework.RunOnFrameworkThread(delegate { ProcessCurrentTrial(); }); } } public string GetStatus() { if (!isProcessing) { return "Idle"; } if (currentTrialIndex >= 0 && currentTrialIndex < Trials.Length) { string name = Trials[currentTrialIndex].Name; if (waitingForQuest) { return "Waiting for " + name + " unlock quest"; } if (waitingForParty) { return "Waiting for party (" + name + ")"; } if (waitingForTrial) { return "In " + name; } return "Processing " + name; } return "Processing..."; } public void Reset() { isProcessing = false; currentTrialIndex = -1; waitingForQuest = false; waitingForParty = false; waitingForTrial = false; submarineManager.SetExternalPause(paused: false); } public void Dispose() { framework.Update -= OnFrameworkUpdate; condition.ConditionChange -= OnConditionChanged; log.Information("[ARRTrials] Service disposed"); } private void OnConditionChanged(ConditionFlag flag, bool value) { if (flag == ConditionFlag.BoundByDuty) { if (value && !isInDuty) { isInDuty = true; log.Debug("[ARRTrials] Entered duty"); } else if (!value && isInDuty) { isInDuty = false; OnDutyExited(); } } } private void OnDutyExited() { if (!isProcessing || !waitingForTrial) { return; } log.Information("[ARRTrials] Exited duty - stopping AD and disbanding..."); framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/ad stop"); }); Task.Run(async delegate { await Task.Delay(2000); framework.RunOnFrameworkThread(delegate { memoryHelper.SendChatMessage("/leave"); commandManager.ProcessCommand("/ad stop"); log.Information("[ARRTrials] /leave and safety /ad stop sent"); }); log.Information("[ARRTrials] Waiting for completion state check..."); await Task.Delay(1000); (uint, uint, string, string) tuple = Trials[currentTrialIndex]; uint trialId = tuple.Item2; for (int i = 0; i < 10; i++) { if (IsTrialComplete(trialId)) { log.Information($"[ARRTrials] Completion verified on attempt {i + 1}"); break; } await Task.Delay(1000); } OnDutyComplete(); }); } }