qstbak/Questionable/Questionable.Controller.Steps.Interactions/SinglePlayerDuty.cs
2026-08-19 13:19:57 +10:00

385 lines
11 KiB
C#

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<ITask> 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<byte> varsBefore = questProgressInfo?.Variables.ToArray();
dutyRetryState.Reset();
List<ITask> dutyTasks = new List<ITask>();
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<StartSinglePlayerDuty>()
{
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<EnableAi>()
{
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<WaitSinglePlayerDuty>(), 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<DisableAi>()
{
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<SetTarget>()
{
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<byte>? VariablesBeforeEntering, IList<QuestWorkValue?> CompletionQuestVariablesFlags, IReadOnlyList<ITask> DutyTasks) : IDutyTask, ITask
{
public override string ToString()
{
return "CheckDutyOutcome";
}
}
internal sealed class CheckDutyOutcomeExecutor(QuestFunctions questFunctions, DutyRetryState retryState, Configuration configuration, ILogger<CheckDutyOutcomeExecutor> logger) : TaskExecutor<CheckDutyOutcome>(), 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<ITask> CreateExtraTasks()
{
foreach (ITask dutyTask in base.Task.DutyTasks)
{
yield return dutyTask;
}
yield return base.Task;
}
public override bool ShouldInterruptOnDamage()
{
return false;
}
}
}