using System; using System.Threading.Tasks; using Dalamud.Game.ClientState.Conditions; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game.Group; using Newtonsoft.Json.Linq; namespace QuestionableCompanion.Services; public class DungeonAutomationService : IDisposable { private readonly ICondition condition; private readonly IPluginLog log; private readonly IClientState clientState; private readonly ICommandManager commandManager; private readonly IFramework framework; private readonly IGameGui gameGui; private readonly Configuration config; private readonly HelperManager helperManager; private readonly MemoryHelper memoryHelper; private readonly QuestionableIPC questionableIPC; private bool isWaitingForParty; private DateTime partyInviteTime = DateTime.MinValue; private int inviteAttempts; private bool isInvitingHelpers; private DateTime helperInviteTime = DateTime.MinValue; private bool isInDuty; private bool hasStoppedAD; private DateTime dutyEntryTime = DateTime.MinValue; private bool pendingAutomationStop; private DateTime lastDutyExitTime = DateTime.MinValue; private DateTime lastDutyEntryTime = DateTime.MinValue; private bool expectingDutyEntry; private bool isAutomationActive; private bool hasSentAtY; public bool IsWaitingForParty => isWaitingForParty; public int CurrentPartySize { get; private set; } = 1; public bool IsInAutoDutyDungeon => isAutomationActive; public DungeonAutomationService(ICondition condition, IPluginLog log, IClientState clientState, ICommandManager commandManager, IFramework framework, IGameGui gameGui, Configuration config, HelperManager helperManager, MemoryHelper memoryHelper, QuestionableIPC questionableIPC) { this.condition = condition; this.log = log; this.clientState = clientState; this.commandManager = commandManager; this.framework = framework; this.gameGui = gameGui; this.config = config; this.helperManager = helperManager; this.memoryHelper = memoryHelper; this.questionableIPC = questionableIPC; condition.ConditionChange += OnConditionChanged; log.Information("[DungeonAutomation] Service initialized with ConditionChange event"); log.Information($"[DungeonAutomation] Config - Required Party Size: {config.AutoDutyPartySize}"); log.Information($"[DungeonAutomation] Config - Party Wait Time: {config.AutoDutyMaxWaitForParty}s"); log.Information($"[DungeonAutomation] Config - Dungeon Automation Enabled: {config.EnableAutoDutyUnsynced}"); SetDutyModeBasedOnConfig(); } public void StartDungeonAutomation() { if (!isAutomationActive) { log.Information("[DungeonAutomation] ========================================"); log.Information("[DungeonAutomation] === STARTING DUNGEON AUTOMATION ==="); log.Information("[DungeonAutomation] ========================================"); isAutomationActive = true; expectingDutyEntry = true; log.Information("[DungeonAutomation] Inviting helpers via HelperManager..."); helperManager.InviteHelpers(); isInvitingHelpers = true; helperInviteTime = DateTime.Now; inviteAttempts = 0; } } public void SetDutyModeBasedOnConfig() { if (config.EnableAutoDutyUnsynced) { questionableIPC.SetDefaultDutyMode(2); log.Information("[DungeonAutomation] Set Duty Mode to Unsync Party (2) - Automation Enabled"); } else { questionableIPC.SetDefaultDutyMode(0); log.Information("[DungeonAutomation] Set Duty Mode to Support (0) - Automation Disabled"); } } public void StopDungeonAutomation() { if (isAutomationActive) { log.Information("[DungeonAutomation] ========================================"); log.Information("[DungeonAutomation] === STOPPING DUNGEON AUTOMATION ==="); log.Information("[DungeonAutomation] ========================================"); isAutomationActive = false; Reset(); } } private void UpdateHelperInvite() { double timeSinceInvite = (DateTime.Now - helperInviteTime).TotalSeconds; try { if (timeSinceInvite >= 2.0) { isInvitingHelpers = false; isWaitingForParty = true; partyInviteTime = DateTime.Now; log.Information("[DungeonAutomation] Helper invites sent, waiting for party..."); } } catch (Exception ex) { log.Error("[DungeonAutomation] Error in helper invite: " + ex.Message); isInvitingHelpers = false; } } public void Update() { if (config.EnableAutoDutyUnsynced && !isAutomationActive) { CheckWaitForPartyTask(); } if (!hasStoppedAD && dutyEntryTime != DateTime.MinValue && (DateTime.Now - dutyEntryTime).TotalSeconds >= 1.0) { try { commandManager.ProcessCommand("/ad stop"); log.Information("[DungeonAutomation] /ad stop (1s after duty entry)"); hasStoppedAD = true; dutyEntryTime = DateTime.MinValue; } catch (Exception ex) { log.Error("[DungeonAutomation] Failed to stop AD: " + ex.Message); } } if (isInvitingHelpers) { UpdateHelperInvite(); } else if (pendingAutomationStop && (DateTime.Now - dutyEntryTime).TotalSeconds >= 5.0) { log.Information("[DungeonAutomation] 5s delay complete - stopping automation now"); StopDungeonAutomation(); pendingAutomationStop = false; } else if (isWaitingForParty) { UpdatePartySize(); if (CurrentPartySize >= config.AutoDutyPartySize) { log.Information("[DungeonAutomation] ========================================"); log.Information("[DungeonAutomation] === PARTY FULL ==="); log.Information("[DungeonAutomation] ========================================"); log.Information($"[DungeonAutomation] Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}"); isWaitingForParty = false; partyInviteTime = DateTime.MinValue; inviteAttempts = 0; log.Information("[DungeonAutomation] Party full - ready for dungeon!"); } else if ((DateTime.Now - partyInviteTime).TotalSeconds >= (double)config.AutoDutyMaxWaitForParty) { log.Warning($"[DungeonAutomation] Party not full after {config.AutoDutyMaxWaitForParty}s - retrying invite (Attempt #{inviteAttempts + 1})"); log.Information($"[DungeonAutomation] Current Party Size: {CurrentPartySize}/{config.AutoDutyPartySize}"); log.Information("[DungeonAutomation] Retrying helper invites..."); helperManager.InviteHelpers(); partyInviteTime = DateTime.Now; } } } private void CheckWaitForPartyTask() { if (questionableIPC.GetCurrentTask() is JObject jObject) { JToken taskNameToken = jObject["TaskName"]; if (taskNameToken != null && taskNameToken.ToString() == "WaitForParty") { StartDungeonAutomation(); } } } private unsafe void UpdatePartySize() { try { int partySize = 0; GroupManager* groupManager = GroupManager.Instance(); if (groupManager != null) { GroupManager.Group* group = groupManager->GetGroup(); if (group != null) { partySize = group->MemberCount; } } if (partySize == 0) { partySize = 1; } if (partySize != CurrentPartySize) { CurrentPartySize = partySize; log.Information($"[DungeonAutomation] Party Size updated: {CurrentPartySize}/{config.AutoDutyPartySize}"); } } catch (Exception ex) { log.Error("[DungeonAutomation] Error updating party size: " + ex.Message); } } private void OnConditionChanged(ConditionFlag flag, bool value) { if (flag == ConditionFlag.BoundByDuty) { if (value && !isInDuty) { isInDuty = true; OnDutyEntered(); } else if (!value && isInDuty) { isInDuty = false; OnDutyExited(); } } } public void OnDutyEntered() { if ((DateTime.Now - lastDutyEntryTime).TotalSeconds < 5.0) { log.Debug("[DungeonAutomation] OnDutyEntered called too soon - ignoring spam"); return; } lastDutyEntryTime = DateTime.Now; log.Information("[DungeonAutomation] Entered duty"); if (expectingDutyEntry) { log.Information("[DungeonAutomation] Duty started by DungeonAutomation - enabling automation commands"); expectingDutyEntry = false; hasStoppedAD = false; dutyEntryTime = DateTime.Now; if (!hasSentAtY) { commandManager.ProcessCommand("/at y"); log.Information("[DungeonAutomation] Sent /at y (duty entered)"); hasSentAtY = true; } } else { log.Information("[DungeonAutomation] Duty NOT started by DungeonAutomation (Solo Duty/Quest Battle) - skipping automation commands"); } } public void OnDutyExited() { if ((DateTime.Now - lastDutyExitTime).TotalSeconds < 2.0) { log.Debug("[DungeonAutomation] OnDutyExited called too soon - ignoring spam"); return; } lastDutyExitTime = DateTime.Now; log.Information("[DungeonAutomation] Exited duty"); if (isAutomationActive) { commandManager.ProcessCommand("/at n"); log.Information("[DungeonAutomation] Sent /at n (duty exited)"); hasSentAtY = false; log.Information("[DungeonAutomation] Waiting 8s, then disband + restart quest"); Task.Run(async delegate { await EnsureSoloPartyAsync(); }); StopDungeonAutomation(); } else { log.Information("[DungeonAutomation] Exited non-automated duty - no cleanup needed"); } } private async Task EnsureSoloPartyAsync() { TimeSpan timeout = TimeSpan.FromSeconds(60L); DateTime start = DateTime.Now; while (CurrentPartySize > 1 && DateTime.Now - start < timeout) { await framework.RunOnFrameworkThread(delegate { commandManager.ProcessCommand("/leave"); }); log.Information("[DungeonAutomation] Forced /leave sent, rechecking party size..."); await Task.Delay(1500); UpdatePartySize(); } if (CurrentPartySize > 1) { log.Warning("[DungeonAutomation] Still not solo after leave spam!"); } else { log.Information("[DungeonAutomation] Party reduced to solo after duty exit."); } } public void DisbandParty() { try { log.Information("[DungeonAutomation] Disbanding party"); framework.RunOnFrameworkThread(delegate { memoryHelper.SendChatMessage("/leave"); log.Information("[DungeonAutomation] /leave command sent via UIModule"); }); } catch (Exception ex) { log.Error("[DungeonAutomation] Failed to disband party: " + ex.Message); } } public void Reset() { isWaitingForParty = false; partyInviteTime = DateTime.MinValue; inviteAttempts = 0; CurrentPartySize = 1; isInvitingHelpers = false; helperInviteTime = DateTime.MinValue; isAutomationActive = false; log.Information("[DungeonAutomation] State reset"); } public void Dispose() { Reset(); condition.ConditionChange -= OnConditionChanged; } }