using System; using System.Collections.Generic; using System.Linq; using System.Numerics; using Dalamud.Game.ClientState.Conditions; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.Game; using FFXIVClientStructs.FFXIV.Client.Game.Event; using FFXIVClientStructs.FFXIV.Client.Game.InstanceContent; 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 SinglePlayerDuty { internal static class SpecialTerritories { public const ushort Lahabrea = 1052; public const ushort ItsProbablyATrap = 665; public const ushort Naadam = 688; } internal sealed class Factory(BossModIpc bossModIpc, WrathComboModule wrathComboModule, RotationSolverRebornModule rsrModule, Configuration configuration, TerritoryData territoryData, QuestFunctions questFunctions, DutyRetryState dutyRetryState, ICondition condition, IClientState clientState, IObjectTable objectTable) : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.SinglePlayerDuty) { yield break; } yield return new Mount.UnmountTask(); if (!bossModIpc.IsConfiguredToRunSoloInstance(quest.Id, step.SinglePlayerDutyOptions)) { yield break; } if (!territoryData.TryGetContentFinderConditionForSoloInstance(quest.Id, step.SinglePlayerDutyIndex, out TerritoryData.ContentFinderConditionData contentFinderConditionData)) { throw new TaskException("Failed to get content finder condition for solo instance"); } QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(quest.Id); byte seqBefore = questProgressInfo?.Sequence ?? 0; IReadOnlyList varsBefore = questProgressInfo?.Variables.ToArray(); dutyRetryState.Reset(); List dutyTasks = new List(); if (wrathComboModule.IsAvailable()) { dutyTasks.Add(new Duty.EnableWrathForDutyTask()); } else if (rsrModule.IsAvailable()) { dutyTasks.Add(new Duty.EnableRsrForDutyTask()); } if (configuration.General.CombatModule == Configuration.ECombatModule.BossMod && bossModIpc.IsSupported()) { dutyTasks.Add(new Duty.EnableBossModForDutyTask()); } else if (bossModIpc.IsSupported()) { dutyTasks.Add(new Duty.EnableBossModPassiveForDutyTask()); } dutyTasks.Add(new StartSinglePlayerDuty(contentFinderConditionData.ContentFinderConditionId)); dutyTasks.Add(new WaitAtStart.WaitDelay(TimeSpan.FromSeconds(2L))); dutyTasks.Add(new EnableAi()); if (contentFinderConditionData.TerritoryId == 1052) { dutyTasks.Add(new SetTarget(14643u)); dutyTasks.Add(new WaitCondition.Task(() => condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(death)")); dutyTasks.Add(new DisableAi()); dutyTasks.Add(new WaitCondition.Task(() => !condition[ConditionFlag.Unconscious] || clientState.TerritoryType != 1052, "Wait(resurrection)")); dutyTasks.Add(new EnableAi()); } else if (contentFinderConditionData.TerritoryId == 665) { dutyTasks.Add(new WaitCondition.Task(() => DutyActionsAvailable() || clientState.TerritoryType != 665, "Wait(Phase 2)")); dutyTasks.Add(new EnableAi()); } else if (contentFinderConditionData.TerritoryId == 688) { dutyTasks.Add(new WaitCondition.Task(delegate { if (clientState.TerritoryType != 688) { return true; } Vector3 vector = objectTable[0]?.Position ?? default(Vector3); return (new Vector3(352.01f, -1.45f, 288.59f) - vector).Length() < 10f; }, "Wait(moving to Ovoo)")); dutyTasks.Add(new Mount.UnmountTask()); dutyTasks.Add(new EnableAi()); } dutyTasks.Add(new WaitSinglePlayerDuty(contentFinderConditionData.ContentFinderConditionId)); dutyTasks.Add(new DisableAi()); dutyTasks.Add(new Duty.DisableCombatPluginsForDutyTask()); foreach (ITask item in dutyTasks) { yield return item; } yield return new CheckDutyOutcome(quest.Id, seqBefore, varsBefore, step.CompletionQuestVariablesFlags, dutyTasks); } private unsafe bool DutyActionsAvailable() { ContentDirector* contentDirector = EventFramework.Instance()->GetContentDirector(); if (contentDirector != null) { return contentDirector->DutyActionManager.ActionsPresent; } return false; } } internal sealed record StartSinglePlayerDuty(uint ContentFinderConditionId) : ITask { public override string ToString() { return $"Wait(BossMod, entered instance {ContentFinderConditionId})"; } } internal sealed class StartSinglePlayerDutyExecutor(ICondition condition) : TaskExecutor() { private long _enteredAtMs; protected override bool Start() { return true; } public unsafe override ETaskResult Update() { if (GameMain.Instance()->CurrentContentFinderConditionId != base.Task.ContentFinderConditionId) { return ETaskResult.StillRunning; } if (!condition[ConditionFlag.BoundByDuty]) { return ETaskResult.StillRunning; } if (_enteredAtMs == 0L) { _enteredAtMs = Environment.TickCount64; } if (Environment.TickCount64 - _enteredAtMs < 2000) { return ETaskResult.StillRunning; } return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record EnableAi : ITask { public override string ToString() { return "BossMod.EnableAi"; } } internal sealed class EnableAiExecutor(BossModIpc bossModIpc) : TaskExecutor() { protected override bool Start() { bossModIpc.EnableQuestBattleAi(); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record WaitSinglePlayerDuty(uint ContentFinderConditionId) : IDutyTask, ITask { public override string ToString() { return $"Wait(BossMod, left instance {ContentFinderConditionId})"; } } internal sealed class WaitSinglePlayerDutyExecutor(BossModIpc bossModIpc, MovementController movementController, TerritoryData territoryData, NotificationService notificationService) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor, IDebugStateProvider { protected override bool Start() { return true; } public unsafe override ETaskResult Update() { if (GameMain.Instance()->CurrentContentFinderConditionId != base.Task.ContentFinderConditionId) { string text = territoryData.GetContentFinderCondition(base.Task.ContentFinderConditionId)?.Name ?? "Single player duty"; notificationService.Notify(EInteractionType.SinglePlayerDuty, "Single player duty complete - " + text); return ETaskResult.TaskComplete; } return ETaskResult.StillRunning; } public void StopNow() { bossModIpc.DisableQuestBattleAi(); } public override bool ShouldInterruptOnDamage() { return false; } public string? GetDebugState() { if (!movementController.IsNavmeshReady) { return $"Navmesh: {movementController.BuiltNavmeshPercent}%"; } return null; } } internal sealed record DisableAi : ITask { public override string ToString() { return "BossMod.DisableAi"; } } internal sealed class DisableAiExecutor(BossModIpc bossModIpc) : TaskExecutor() { protected override bool Start() { bossModIpc.DisableQuestBattleAi(); return true; } public override ETaskResult Update() { return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record SetTarget(uint DataId) : ITask { public override string ToString() { return $"SetTarget({DataId})"; } } internal sealed class SetTargetExecutor(ITargetManager targetManager, GameFunctions gameFunctions) : TaskExecutor() { protected override bool Start() { return true; } public override ETaskResult Update() { if (targetManager.Target?.BaseId == base.Task.DataId) { return ETaskResult.TaskComplete; } IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.DataId, null, warnIfMissing: false); if (gameObject == null) { return ETaskResult.StillRunning; } targetManager.Target = gameObject; return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record CheckDutyOutcome(ElementId QuestId, byte SequenceBeforeEntering, IReadOnlyList? VariablesBeforeEntering, IList CompletionQuestVariablesFlags, IReadOnlyList DutyTasks) : IDutyTask, ITask { public override string ToString() { return "CheckDutyOutcome"; } } internal sealed class CheckDutyOutcomeExecutor(QuestFunctions questFunctions, DutyRetryState retryState, Configuration configuration, ILogger logger) : TaskExecutor(), IExtraTaskCreator, ITaskExecutor { private long _checkAtMs; protected override bool Start() { _checkAtMs = Environment.TickCount64 + 2000; retryState.BeginAwaitingOutcome(); return true; } public override ETaskResult Update() { if (Environment.TickCount64 < _checkAtMs) { return ETaskResult.StillRunning; } string text = DetectSuccess(); if (text != null) { logger.LogInformation("Duty succeeded ({Reason})", text); retryState.Reset(); return ETaskResult.TaskComplete; } int maxRetries = configuration.SinglePlayerDuties.MaxRetries; if (!retryState.CanRetry(maxRetries)) { logger.LogInformation("Duty failed, retry limit reached or retries disabled"); retryState.EndAwaitingOutcome(); return ETaskResult.End; } retryState.RecordRetry(); logger.LogInformation("Duty failed, retrying"); return ETaskResult.CreateNewTasks; } private string? DetectSuccess() { QuestProgressInfo questProgressInfo = questFunctions.GetQuestProgressInfo(base.Task.QuestId); if (questProgressInfo == null) { return "quest completed"; } if (questProgressInfo.Sequence > base.Task.SequenceBeforeEntering) { return $"sequence {base.Task.SequenceBeforeEntering} -> {questProgressInfo.Sequence}"; } if (QuestWorkUtils.HasCompletionFlags(base.Task.CompletionQuestVariablesFlags) && QuestWorkUtils.MatchesQuestWork(base.Task.CompletionQuestVariablesFlags, questProgressInfo)) { return "completion flags matched"; } if (base.Task.VariablesBeforeEntering != null && !questProgressInfo.Variables.SequenceEqual(base.Task.VariablesBeforeEntering)) { return "quest variables changed"; } return null; } public IEnumerable CreateExtraTasks() { foreach (ITask dutyTask in base.Task.DutyTasks) { yield return dutyTask; } yield return base.Task; } public override bool ShouldInterruptOnDamage() { return false; } } }