using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Group; using LLib.Gear; using Microsoft.Extensions.Logging; using Questionable.Controller.CombatModules; using Questionable.Controller.Steps.Common; using Questionable.Controller.Steps.Shared; using Questionable.Controller.Utils; using Questionable.Data; using Questionable.External; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; using Questionable.Notifications; namespace Questionable.Controller.Steps.Interactions; internal static class Duty { internal sealed class Factory(AutoDutyIpc autoDutyIpc, WrathComboModule wrathComboModule, RotationSolverRebornModule rsrModule, BossModIpc bossModIpc, Configuration configuration, TerritoryData territoryData, GearStatsCalculator gearStatsCalculator, IObjectTable objectTable, ILogger logger) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.Duty) { yield break; } ArgumentNullException.ThrowIfNull(step.DutyOptions, "step.DutyOptions"); uint cfcId = step.DutyOptions.ContentFinderConditionId; DutyRegistry.Duties.TryGetValue(cfcId, out DutyEntry value); EDutyMode? registryDutyMode = value?.DutyMode; if (autoDutyIpc.IsConfiguredToRunContent(step.DutyOptions)) { EDutyMode effectiveEDutyMode = GetEffectiveEDutyMode(cfcId, registryDutyMode); AutoDutyIpc.DutyMode dutyMode = ConvertToAutoDutyMode(effectiveEDutyMode); if (dutyMode == AutoDutyIpc.DutyMode.UnsyncRegular && effectiveEDutyMode == EDutyMode.UnsyncParty) { yield return new WaitForPartyTask(); } if (!autoDutyIpc.ManagesRotationPluginState()) { if (wrathComboModule.IsAvailable()) { yield return new EnableWrathForDutyTask(); } else if (rsrModule.IsAvailable()) { yield return new EnableRsrForDutyTask(); } } if (!autoDutyIpc.ManagesBossModAiSettings()) { if (configuration.General.CombatModule == Configuration.ECombatModule.BossMod && bossModIpc.IsSupported()) { yield return new EnableBossModForDutyTask(); } else if (bossModIpc.IsSupported()) { yield return new EnableBossModPassiveForDutyTask(); } } yield return new StartAutoDutyTask(cfcId, dutyMode); yield return new WaitAutoDutyTask(cfcId); yield return new DisableCombatPluginsForDutyTask(); if (!QuestWorkUtils.HasCompletionFlags(step.CompletionQuestVariablesFlags)) { yield return new WaitAtEnd.WaitNextStepOrSequence(); } } else if ((object)value == null || !value.LowPriority) { yield return new OpenDutyFinderTask(cfcId); } } private EDutyMode GetEffectiveEDutyMode(uint cfcId, EDutyMode? registryDutyMode) { if (registryDutyMode.HasValue) { return registryDutyMode.Value; } if (configuration.Duties.DutyModeOverrides.TryGetValue(cfcId, out var value)) { return value; } EDutyMode defaultDutyMode = configuration.Duties.DefaultDutyMode; if (defaultDutyMode == EDutyMode.Support && configuration.Duties.AutoUnsyncOverleveled && IsSafelyOverleveled(cfcId)) { logger.LogInformation("Running duty {CfcId} unsynced, as the player is safely overleveled", cfcId); return EDutyMode.UnsyncSolo; } return defaultDutyMode; } private unsafe bool IsSafelyOverleveled(uint cfcId) { if (!territoryData.TryGetContentFinderCondition(cfcId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { return false; } if (!contentFinderConditionData.IsDungeon || !contentFinderConditionData.AllowUndersized || contentFinderConditionData.ClassJobLevelSync == 0) { return false; } int num = (objectTable.LocalPlayer?.Level ?? 0) - contentFinderConditionData.ClassJobLevelSync; if (num >= 15) { return true; } if (num < 10) { return false; } InventoryManager* ptr = InventoryManager.Instance(); if (ptr == null) { return false; } InventoryContainer* inventoryContainer = ptr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { return false; } return gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer) - contentFinderConditionData.RequiredItemLevel >= 100; } private static AutoDutyIpc.DutyMode ConvertToAutoDutyMode(EDutyMode mode) { return mode switch { EDutyMode.Support => AutoDutyIpc.DutyMode.Support, EDutyMode.UnsyncSolo => AutoDutyIpc.DutyMode.UnsyncRegular, EDutyMode.UnsyncParty => AutoDutyIpc.DutyMode.UnsyncRegular, _ => AutoDutyIpc.DutyMode.Support, }; } } internal sealed record WaitForPartyTask : ITask { public override string ToString() { return "WaitForParty"; } } internal sealed class WaitForPartyExecutor(IChatGui chatGui, ILogger logger) : TaskExecutor() { private long _warnNextMs; private long _startMs; protected override bool Start() { logger.LogDebug("Waiting for party members before starting duty..."); _startMs = Environment.TickCount64; return true; } public unsafe override ETaskResult Update() { GroupManager* ptr = GroupManager.Instance(); if (ptr == null) { return ETaskResult.StillRunning; } byte memberCount = ptr->MainGroup.MemberCount; bool isAlliance = ptr->MainGroup.IsAlliance; if (memberCount > 1 || isAlliance) { logger.LogDebug("Party detected with {MemberCount} members, proceeding with duty", memberCount); return ETaskResult.TaskComplete; } long tickCount = Environment.TickCount64; if (tickCount - _startMs > 300000) { logger.LogError("No party members joined within 5 minutes, stopping"); chatGui.PrintError("No party members joined within 5 minutes, stopping.", "Questionable", 576); return ETaskResult.End; } if (tickCount >= _warnNextMs) { chatGui.Print("[Questionable] Waiting for party members before starting duty (Unsync Party mode)...", "Questionable", 576); _warnNextMs = tickCount + 10000; } return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record StartAutoDutyTask(uint ContentFinderConditionId, AutoDutyIpc.DutyMode DutyMode) : ITask { public override string ToString() { return $"StartAutoDuty({ContentFinderConditionId}, {DutyMode})"; } } internal sealed class StartAutoDutyExecutor(GearStatsCalculator gearStatsCalculator, AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, ILogger logger, SendNotification.Executor sendNotificationExecutor) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private long _startMs; private bool _runAccepted; protected unsafe override bool Start() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } if (base.Task.DutyMode != AutoDutyIpc.DutyMode.UnsyncRegular) { InventoryManager* intPtr = InventoryManager.Instance(); if (intPtr == null) { throw new TaskException("Inventory unavailable"); } InventoryContainer* inventoryContainer = intPtr->GetInventoryContainer(InventoryType.EquippedItems); if (inventoryContainer == null) { throw new TaskException("Equipped items unavailable"); } short num = gearStatsCalculator.CalculateAverageItemLevel(inventoryContainer); if (contentFinderConditionData.RequiredItemLevel > num) { string text = $"Could not use AutoDuty to queue for {contentFinderConditionData.Name}, required item level: {contentFinderConditionData.RequiredItemLevel}, current item level: {num}."; if (!sendNotificationExecutor.Start(new SendNotification.Task(EInteractionType.Duty, text))) { chatGui.PrintError(text, "Questionable", 576); } return false; } } autoDutyIpc.StartInstance(base.Task.ContentFinderConditionId, base.Task.DutyMode); _startMs = Environment.TickCount64; _runAccepted = false; return true; } public override ETaskResult Update() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } if (clientState.TerritoryType == contentFinderConditionData.TerritoryId) { return ETaskResult.TaskComplete; } if (!autoDutyIpc.IsStopped()) { _runAccepted = true; return ETaskResult.StillRunning; } if (!_runAccepted) { logger.LogError("AutoDuty did not start duty {CfcId} (territory {TerritoryId}) - Run was rejected, check the AutoDuty log [{Installation}]", base.Task.ContentFinderConditionId, contentFinderConditionData.TerritoryId, autoDutyIpc.DescribeInstallation()); chatGui.PrintError("AutoDuty did not start the duty (rejected the run), check the AutoDuty log.", "Questionable", 576); return ETaskResult.End; } if (Environment.TickCount64 - _startMs < 10000) { return ETaskResult.StillRunning; } logger.LogError("AutoDuty stopped without entering duty {CfcId} after 10 seconds", base.Task.ContentFinderConditionId); chatGui.PrintError("AutoDuty failed to start the duty, stopping.", "Questionable", 576); return ETaskResult.End; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record WaitAutoDutyTask(uint ContentFinderConditionId) : IDutyTask, ITask { public override string ToString() { return $"Wait(AutoDuty, left instance {ContentFinderConditionId})"; } } internal sealed class WaitAutoDutyExecutor(AutoDutyIpc autoDutyIpc, TerritoryData territoryData, IClientState clientState, IChatGui chatGui, ILogger logger, NotificationService notificationService) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private long _stoppedInsideMs = -1L; protected override bool Start() { _stoppedInsideMs = -1L; return true; } public override ETaskResult Update() { if (!territoryData.TryGetContentFinderCondition(base.Task.ContentFinderConditionId, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get territory ID for content finder condition"); } bool flag = clientState.TerritoryType != contentFinderConditionData.TerritoryId; if (flag && autoDutyIpc.IsStopped()) { notificationService.Notify(EInteractionType.Duty, "Duty complete - " + contentFinderConditionData.Name); return ETaskResult.TaskComplete; } if (!flag && autoDutyIpc.IsStopped()) { long tickCount = Environment.TickCount64; if (_stoppedInsideMs < 0) { _stoppedInsideMs = tickCount; } if (tickCount - _stoppedInsideMs > 60000) { logger.LogError("AutoDuty stopped inside duty {CfcId} and did not progress within 60s", base.Task.ContentFinderConditionId); chatGui.PrintError("AutoDuty stopped inside the duty, stopping.", "Questionable", 576); return ETaskResult.End; } } else { _stoppedInsideMs = -1L; } return ETaskResult.StillRunning; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record OpenDutyFinderTask(uint ContentFinderConditionId) : ITask { public override string ToString() { return $"OpenDutyFinder({ContentFinderConditionId})"; } } internal sealed class OpenDutyFinderExecutor(GameFunctions gameFunctions, ICondition condition) : TaskExecutor() { protected override bool Start() { if (condition[ConditionFlag.InDutyQueue]) { return false; } gameFunctions.OpenDutyFinder(base.Task.ContentFinderConditionId); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record EnableBossModForDutyTask : ITask { public override string ToString() { return "EnableBossModForDuty"; } } internal sealed class EnableBossModForDutyExecutor(BossModIpc bossModIpc, IObjectTable objectTable, ILogger logger) : TaskExecutor() { protected override bool Start() { bossModIpc.EnableAi(BossModIpc.EPreset.Active); IPlayerCharacter localPlayer = objectTable.LocalPlayer; bool flag; if (localPlayer != null) { byte? b = localPlayer.ClassJob.ValueNullable?.Role; if (b.HasValue) { byte valueOrDefault = b.GetValueOrDefault(); if ((uint)(valueOrDefault - 3) <= 1u) { flag = true; goto IL_0077; } } flag = false; goto IL_0077; } goto IL_0087; IL_0077: bool isRanged = flag; bossModIpc.SetRangeStrategy(BossModIpc.EPreset.Active, isRanged); goto IL_0087; IL_0087: logger.LogDebug("Enabled BossMod Active preset for AutoDuty run"); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record EnableRsrForDutyTask : ITask { public override string ToString() { return "EnableRsrForDuty"; } } internal sealed class EnableRsrForDutyExecutor(RotationSolverRebornModule rsrModule, ILogger logger) : TaskExecutor() { protected override bool Start() { if (rsrModule.StartForDuty()) { logger.LogDebug("Enabled RSR Henched mode for AutoDuty run"); return true; } logger.LogWarning("Failed to enable RSR for AutoDuty run"); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record EnableBossModPassiveForDutyTask : ITask { public override string ToString() { return "EnableBossModPassiveForDuty"; } } internal sealed class EnableBossModPassiveForDutyExecutor(BossModIpc bossModIpc, ILogger logger) : TaskExecutor() { protected override bool Start() { bossModIpc.EnableAi(BossModIpc.EPreset.Passive); logger.LogDebug("Enabled BossMod Passive preset for AutoDuty run (hazard avoidance)"); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record EnableWrathForDutyTask : ITask { public override string ToString() { return "EnableWrathForDuty"; } } internal sealed class EnableWrathForDutyExecutor(WrathComboModule wrathComboModule, ILogger logger) : TaskExecutor() { protected override bool Start() { CombatController.CombatData combatData = new CombatController.CombatData { ElementId = null, Sequence = 0, CompletionQuestVariablesFlags = new List(), SpawnType = EEnemySpawnType.None, KillEnemyDataIds = new List(), ComplexCombatDatas = new List(), CombatItemUse = null }; if (wrathComboModule.Start(combatData)) { logger.LogDebug("Enabled WrathCombo for AutoDuty run"); return true; } logger.LogWarning("Failed to enable WrathCombo for AutoDuty run"); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record DisableCombatPluginsForDutyTask : ITask { public override string ToString() { return "DisableCombatPluginsForDuty"; } } internal sealed class DisableCombatPluginsForDutyExecutor(WrathComboModule wrathComboModule, RotationSolverRebornModule rsrModule, BossModIpc bossModIpc, ILogger logger) : TaskExecutor() { protected override bool Start() { wrathComboModule.Stop(); rsrModule.Stop(); bossModIpc.DisableAi(); logger.LogDebug("Disabled combat plugins after AutoDuty run"); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record StartLevelingModeTask(int RequiredLevel, string? QuestName) : ITask { public override string ToString() { return $"StartLevelingMode(target: Lv{RequiredLevel} for '{QuestName}')"; } } internal sealed class StartLevelingModeExecutor(AutoDutyIpc autoDutyIpc, ICondition condition, IChatGui chatGui, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private long _startMs; protected override bool Start() { logger.LogInformation("Starting AutoDuty Leveling mode to reach level {RequiredLevel} for quest '{QuestName}'", base.Task.RequiredLevel, base.Task.QuestName); if (condition[ConditionFlag.BoundByDuty] || condition[ConditionFlag.InDutyQueue]) { logger.LogDebug("Already in duty or queue, skipping start"); return true; } if (!autoDutyIpc.IsStopped()) { logger.LogDebug("AutoDuty is already running, waiting for it"); _startMs = Environment.TickCount64; return true; } autoDutyIpc.StartLevelingMode(); _startMs = Environment.TickCount64; return true; } public override ETaskResult Update() { bool flag = condition[ConditionFlag.BoundByDuty]; bool flag2 = condition[ConditionFlag.InDutyQueue]; if (flag || flag2) { logger.LogDebug("AutoDuty started successfully (inDuty={InDuty}, inQueue={InQueue})", flag, flag2); return ETaskResult.TaskComplete; } if (!autoDutyIpc.IsStopped()) { return ETaskResult.StillRunning; } if (Environment.TickCount64 - _startMs < 10000) { return ETaskResult.StillRunning; } logger.LogError("AutoDuty failed to start leveling mode after 10 seconds"); chatGui.PrintError("AutoDuty failed to start leveling mode, stopping.", "Questionable", 576); return ETaskResult.End; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record WaitLevelingModeTask(int RequiredLevel) : ITask { public override string ToString() { return $"WaitLevelingMode(until Lv{RequiredLevel})"; } } internal sealed class WaitLevelingModeExecutor(AutoDutyIpc autoDutyIpc, IObjectTable objectTable, ICondition condition, IChatGui chatGui, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor { private bool _wasInDuty; private long _statusLogNextMs; private long _idleSinceMs = -1L; private long _reissueNextMs; protected override bool Start() { _wasInDuty = false; _statusLogNextMs = 0L; _idleSinceMs = -1L; _reissueNextMs = 0L; return true; } public override ETaskResult Update() { bool flag = condition[ConditionFlag.BoundByDuty]; bool flag2 = condition[ConditionFlag.InDutyQueue]; if (flag && !_wasInDuty) { logger.LogDebug("Entered duty for leveling"); _wasInDuty = true; } byte b = objectTable.LocalPlayer?.Level ?? 0; if (b >= base.Task.RequiredLevel) { logger.LogInformation("Reached required level {RequiredLevel} (current: {CurrentLevel})", base.Task.RequiredLevel, b); chatGui.Print($"Reached level {b}, can now continue MSQ.", "Questionable", 576); autoDutyIpc.Stop(); return ETaskResult.TaskComplete; } if (autoDutyIpc.IsStopped() && !flag && !flag2 && _wasInDuty) { long tickCount = Environment.TickCount64; if (_idleSinceMs < 0) { _idleSinceMs = tickCount; } if (tickCount >= _statusLogNextMs) { int num = base.Task.RequiredLevel - b; logger.LogDebug("Leveling between dungeons (current: {CurrentLevel}, need: {RequiredLevel}, {LevelsNeeded} more levels needed)", b, base.Task.RequiredLevel, num); _statusLogNextMs = tickCount + 30000; } if (tickCount - _idleSinceMs > 90000) { logger.LogError("AutoDuty leveling stalled at level {CurrentLevel} (need {RequiredLevel}) and did not resume within 90s", b, base.Task.RequiredLevel); chatGui.PrintError("AutoDuty leveling stalled and didn't resume, stopping.", "Questionable", 576); return ETaskResult.End; } if (!condition[ConditionFlag.BetweenAreas] && !condition[ConditionFlag.BetweenAreas51] && tickCount >= _reissueNextMs) { logger.LogDebug("Starting next leveling dungeon"); autoDutyIpc.StartLevelingMode(); _reissueNextMs = tickCount + 5000; } } else { _idleSinceMs = -1L; } return ETaskResult.StillRunning; } public void StopNow() { autoDutyIpc.Stop(); } public override bool ShouldInterruptOnDamage() { return false; } } }