385 lines
11 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|