using System; using System.Collections.Generic; using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using Microsoft.Extensions.Logging; using Questionable.Functions; using Questionable.Model; using Questionable.Model.Questing; namespace Questionable.Controller.Steps.Interactions; internal static class ClearObjectsWithAction { internal sealed class Factory : ITaskFactory { public IEnumerable CreateAllTasks(Quest quest, QuestSequence sequence, QuestStep step) { if (step.InteractionType != EInteractionType.Action) { return Array.Empty(); } if (step.DataIds.Count == 0) { return Array.Empty(); } if (!step.Action.HasValue) { return Array.Empty(); } return new global::_003C_003Ez__ReadOnlySingleElementList(new ClearTask(step.DataIds, step.Action.Value, step.TerritoryId, step.CalculateActualStopDistance(), step.WaypointPositions)); } } internal sealed record ClearTask(List DataIds, EAction Action, ushort TerritoryId, float StopDistance, List WaypointPositions) : ITask { public bool ShouldRedoOnInterrupt() { return true; } public override string ToString() { return $"ClearObjects({Action}, {DataIds.Count} types)"; } } internal sealed class ClearTaskExecutor(MovementController movementController, GameFunctions gameFunctions, IClientState clientState, IObjectTable objectTable, ILogger logger) : TaskExecutor() { private enum State { FindingObject, MovingToObject, UsingAction, WaitingAfterAction, MovingToWaypoint, WaitingForTransition } private const float VisitedPositionRadius = 5f; private State _state; private DateTime _noObjectFoundSince = DateTime.MaxValue; private DateTime _waitUntil = DateTime.MinValue; private DateTime _waypointReachedAt = DateTime.MaxValue; private Vector3 _lastPlayerPosition; private readonly List _visitedPositions = new List(); private readonly List _triedWaypointIndices = new List(); protected override bool Start() { if (clientState.TerritoryType != base.Task.TerritoryId) { return true; } return TryFindAndMove(); } private bool IsVisitedPosition(Vector3 position) { foreach (Vector3 visitedPosition in _visitedPositions) { if (Vector3.Distance(visitedPosition, position) <= 5f) { return true; } } return false; } private IGameObject? FindNearestUnvisitedObject() { IGameObject gameObject = objectTable[0]; if (gameObject == null) { return null; } IGameObject result = null; float num = float.MaxValue; foreach (IGameObject item in objectTable) { ObjectKind objectKind = item.ObjectKind; if ((objectKind == ObjectKind.Player || objectKind - 8 <= ObjectKind.BattleNpc || objectKind == ObjectKind.Housing) ? true : false) { continue; } bool flag = false; foreach (uint dataId in base.Task.DataIds) { if (item.BaseId == dataId) { flag = true; break; } } if (flag && !IsVisitedPosition(item.Position)) { float num2 = Vector3.Distance(gameObject.Position, item.Position); if (num2 < num) { result = item; num = num2; } } } return result; } private bool AnyMatchingObjectsExist() { foreach (IGameObject item in objectTable) { ObjectKind objectKind = item.ObjectKind; if ((objectKind == ObjectKind.Player || objectKind - 8 <= ObjectKind.BattleNpc || objectKind == ObjectKind.Housing) ? true : false) { continue; } foreach (uint dataId in base.Task.DataIds) { if (item.BaseId == dataId) { return true; } } } return false; } private bool TryFindAndMove() { IGameObject gameObject = FindNearestUnvisitedObject(); if (gameObject == null) { _state = State.FindingObject; if (_noObjectFoundSince == DateTime.MaxValue) { _noObjectFoundSince = DateTime.Now; } return true; } _noObjectFoundSince = DateTime.MaxValue; Vector3 position = gameObject.Position; logger.LogInformation("Moving to object {DataId} at {Position}", gameObject.BaseId, position); movementController.NavigateTo(EMovementType.Quest, gameObject.BaseId, position, fly: false, sprint: true, base.Task.StopDistance); _state = State.MovingToObject; return true; } private (Vector3 Position, int Index)? FindNextWaypoint() { IGameObject gameObject = objectTable[0]; if (gameObject == null || base.Task.WaypointPositions.Count == 0) { return null; } (Vector3, int)? result = null; float num = float.MaxValue; for (int i = 0; i < base.Task.WaypointPositions.Count; i++) { if (!_triedWaypointIndices.Contains(i)) { float num2 = Vector3.Distance(gameObject.Position, base.Task.WaypointPositions[i]); if (num2 < num) { result = (base.Task.WaypointPositions[i], i); num = num2; } } } return result; } private void NavigateToNextWaypoint() { (Vector3, int)? tuple = FindNextWaypoint(); if (!tuple.HasValue) { logger.LogInformation("All waypoints tried without transition, resetting"); _triedWaypointIndices.Clear(); tuple = FindNextWaypoint(); if (!tuple.HasValue) { logger.LogWarning("No waypoint positions configured, waiting for transition"); _state = State.WaitingForTransition; _lastPlayerPosition = objectTable[0]?.Position ?? Vector3.Zero; return; } } _triedWaypointIndices.Add(tuple.Value.Item2); logger.LogInformation("Moving to waypoint {Index} at {Position}", tuple.Value.Item2, tuple.Value.Item1); movementController.NavigateTo(EMovementType.Quest, null, tuple.Value.Item1, fly: false, sprint: true, 0f); _state = State.MovingToWaypoint; } public override ETaskResult Update() { if (clientState.TerritoryType != base.Task.TerritoryId) { return ETaskResult.TaskComplete; } switch (_state) { case State.FindingObject: if (_noObjectFoundSince != DateTime.MaxValue && DateTime.Now > _noObjectFoundSince.AddSeconds(3.0)) { if (AnyMatchingObjectsExist()) { logger.LogInformation("Matching objects still exist, clearing visited positions and retrying"); _visitedPositions.Clear(); _noObjectFoundSince = DateTime.MaxValue; } else { logger.LogInformation("No matching objects remain, moving to waypoint"); NavigateToNextWaypoint(); } return ETaskResult.StillRunning; } TryFindAndMove(); return ETaskResult.StillRunning; case State.MovingToObject: { if (movementController.IsPathfinding || movementController.IsPathRunning) { return ETaskResult.StillRunning; } DateTime movementStartedAt = movementController.MovementStartedAt; if (movementStartedAt == DateTime.MaxValue || movementStartedAt.AddSeconds(2.0) >= DateTime.Now) { return ETaskResult.StillRunning; } IGameObject gameObject2 = FindNearestUnvisitedObject(); IGameObject gameObject3 = objectTable[0]; if (gameObject2 != null && gameObject3 != null && Vector3.Distance(gameObject3.Position, gameObject2.Position) <= 5f) { _visitedPositions.Add(gameObject2.Position); _state = State.UsingAction; } else { logger.LogInformation("Target no longer valid at destination, finding next"); TryFindAndMove(); } return ETaskResult.StillRunning; } case State.UsingAction: if (gameFunctions.UseAction(base.Task.Action)) { logger.LogInformation("Used action {Action}", base.Task.Action); _waitUntil = DateTime.Now.AddSeconds(1.0); _state = State.WaitingAfterAction; } return ETaskResult.StillRunning; case State.WaitingAfterAction: if (DateTime.Now < _waitUntil) { return ETaskResult.StillRunning; } TryFindAndMove(); return ETaskResult.StillRunning; case State.MovingToWaypoint: { if (movementController.IsPathfinding || movementController.IsPathRunning) { return ETaskResult.StillRunning; } DateTime movementStartedAt2 = movementController.MovementStartedAt; if (movementStartedAt2 == DateTime.MaxValue || movementStartedAt2.AddSeconds(2.0) >= DateTime.Now) { return ETaskResult.StillRunning; } logger.LogInformation("Reached waypoint, waiting for zone transition"); _state = State.WaitingForTransition; _lastPlayerPosition = objectTable[0]?.Position ?? Vector3.Zero; _waypointReachedAt = DateTime.Now; return ETaskResult.StillRunning; } case State.WaitingForTransition: { IGameObject gameObject = objectTable[0]; if (gameObject != null) { float num = Vector3.Distance(_lastPlayerPosition, gameObject.Position); if (num > 50f) { logger.LogInformation("Zone transition detected (moved {Distance}), resuming plant clearing", num); _visitedPositions.Clear(); _triedWaypointIndices.Clear(); _noObjectFoundSince = DateTime.MaxValue; _state = State.FindingObject; return ETaskResult.StillRunning; } } if (FindNearestUnvisitedObject() != null) { logger.LogInformation("New objects detected, resuming plant clearing"); _visitedPositions.Clear(); _triedWaypointIndices.Clear(); _noObjectFoundSince = DateTime.MaxValue; TryFindAndMove(); return ETaskResult.StillRunning; } if (_waypointReachedAt != DateTime.MaxValue && DateTime.Now > _waypointReachedAt.AddSeconds(3.0)) { logger.LogInformation("No transition at this waypoint, trying next"); NavigateToNextWaypoint(); } return ETaskResult.StillRunning; } default: return ETaskResult.TaskComplete; } } public override bool ShouldInterruptOnDamage() { return false; } } }