using System; using System.Collections.Generic; using Dalamud.Game.ClientState.Objects.Types; using Microsoft.Extensions.Logging; using Questionable.Functions; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class FateFarming { internal sealed record WaitForFateTargets(IReadOnlyList Targets) : ITask { public override string ToString() { return $"WaitForFateTargets({Targets.Count} targets)"; } } internal sealed class WaitForFateTargetsExecutor(GameFunctions gameFunctions, ILogger logger) : TaskExecutor() { private DateTime _nextPollAt = DateTime.MinValue; private bool _loggedWaitingForFate; protected override bool Start() { logger.LogInformation("Waiting for FATE targets to appear ({Count} targets)", base.Task.Targets.Count); _loggedWaitingForFate = false; return true; } public override ETaskResult Update() { if (DateTime.Now < _nextPollAt) { return ETaskResult.StillRunning; } ushort currentFateId = gameFunctions.GetCurrentFateId(); if (currentFateId == 0) { if (!_loggedWaitingForFate) { logger.LogInformation("No active FATE yet, waiting for FATE to start before checking targets"); _loggedWaitingForFate = true; } _nextPollAt = DateTime.Now.AddSeconds(1.0); return ETaskResult.StillRunning; } _loggedWaitingForFate = false; foreach (FateActionTarget target in base.Task.Targets) { IGameObject gameObject = gameFunctions.FindObjectByDataId(target.DataId, null, warnIfMissing: false); if (gameObject != null && gameObject.IsTargetable) { logger.LogInformation("FATE {FateId} active and target {DataId} is targetable", currentFateId, target.DataId); return ETaskResult.TaskComplete; } } _nextPollAt = DateTime.Now.AddSeconds(1.0); return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record SyncFateLevel : ITask { public override string ToString() { return "SyncFateLevel"; } } internal sealed class SyncFateLevelExecutor(GameFunctions gameFunctions, ILogger logger) : TaskExecutor() { protected override bool Start() { logger.LogInformation("Syncing to FATE level"); return true; } public override ETaskResult Update() { ushort currentFateId = gameFunctions.GetCurrentFateId(); if (currentFateId == 0) { logger.LogDebug("No active FATE to sync to, skipping"); return ETaskResult.TaskComplete; } gameFunctions.SyncToFate(currentFateId); return ETaskResult.TaskComplete; } public override bool ShouldInterruptOnDamage() { return false; } } internal sealed record FateActionLoop(IReadOnlyList Targets, EStatus? RequiredStatusId = null) : ITask { public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return $"FateActionLoop({Targets.Count} targets)"; } } internal sealed class FateActionLoopExecutor(GameFunctions gameFunctions, ILogger logger) : TaskExecutor() { private DateTime _nextActionAt = DateTime.MinValue; private ushort _trackedFateId; protected override bool Start() { _trackedFateId = gameFunctions.GetCurrentFateId(); logger.LogInformation("Starting FATE action loop with {Count} targets, tracking FATE {FateId}", base.Task.Targets.Count, _trackedFateId); return true; } public override ETaskResult Update() { if (DateTime.Now < _nextActionAt) { return ETaskResult.StillRunning; } if (base.Task.RequiredStatusId.HasValue && !gameFunctions.HasStatus(base.Task.RequiredStatusId.Value)) { logger.LogInformation("Required status {StatusId} lost during FATE action loop, ending cycle to re-apply", base.Task.RequiredStatusId.Value); return ETaskResult.TaskComplete; } if (_trackedFateId == 0) { _trackedFateId = gameFunctions.GetCurrentFateId(); if (_trackedFateId != 0) { logger.LogInformation("Now tracking FATE {FateId}", _trackedFateId); } } if (_trackedFateId != 0 && !gameFunctions.IsFateStillActive(_trackedFateId)) { logger.LogInformation("FATE {FateId} is no longer running, cycle complete", _trackedFateId); return ETaskResult.TaskComplete; } foreach (FateActionTarget target in base.Task.Targets) { IGameObject gameObject = gameFunctions.FindObjectByDataId(target.DataId, null, warnIfMissing: false); if (gameObject != null && gameObject.IsTargetable) { bool flag = gameFunctions.UseAction(gameObject, target.Action); _nextActionAt = (flag ? DateTime.Now.AddSeconds(2.5) : DateTime.Now.AddSeconds(0.5)); return ETaskResult.StillRunning; } } _nextActionAt = DateTime.Now.AddSeconds(0.25); return ETaskResult.StillRunning; } public override bool ShouldInterruptOnDamage() { return false; } } }