using System; using System.Collections.Generic; using System.Numerics; using Dalamud.Game.ClientState.Objects.Enums; using Dalamud.Game.ClientState.Objects.SubKinds; 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 long? _noObjectFoundSince; private long _waitUntil; private long? _waypointReachedAt; 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() { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer == null) { return null; } IGameObject result = null; float num = float.MaxValue; foreach (IGameObject item in objectTable) { ObjectKind objectKind = item.ObjectKind; bool flag = ((objectKind == ObjectKind.Pc || objectKind - 8 <= ObjectKind.BattleNpc || objectKind == ObjectKind.HousingEventObject) ? true : false); if (!flag && base.Task.DataIds.Contains(item.BaseId) && !IsVisitedPosition(item.Position)) { float num2 = Vector3.Distance(localPlayer.Position, item.Position); if (num2 < num) { result = item; num = num2; } } } return result; } private bool AnyMatchingObjectsExist() { foreach (IGameObject item in objectTable) { ObjectKind objectKind = item.ObjectKind; bool flag = ((objectKind == ObjectKind.Pc || objectKind - 8 <= ObjectKind.BattleNpc || objectKind == ObjectKind.HousingEventObject) ? true : false); if (!flag && base.Task.DataIds.Contains(item.BaseId)) { return true; } } return false; } private bool TryFindAndMove() { IGameObject gameObject = FindNearestUnvisitedObject(); if (gameObject == null) { _state = State.FindingObject; long valueOrDefault = _noObjectFoundSince.GetValueOrDefault(); if (!_noObjectFoundSince.HasValue) { valueOrDefault = Environment.TickCount64; _noObjectFoundSince = valueOrDefault; } return true; } _noObjectFoundSince = null; Vector3 position = gameObject.Position; logger.LogDebug("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() { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer == 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(localPlayer.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.LogDebug("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.LocalPlayer?.Position ?? Vector3.Zero; return; } } _triedWaypointIndices.Add(tuple.Value.Item2); logger.LogDebug("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: { long? waypointReachedAt = _noObjectFoundSince; if (waypointReachedAt.HasValue) { long valueOrDefault2 = waypointReachedAt.GetValueOrDefault(); if (Environment.TickCount64 > valueOrDefault2 + 3000) { if (AnyMatchingObjectsExist()) { logger.LogDebug("Matching objects still exist, clearing visited positions and retrying"); _visitedPositions.Clear(); _noObjectFoundSince = null; } else { logger.LogDebug("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; } if (!movementController.HasBeenMovingForAtLeast(2000)) { return ETaskResult.StillRunning; } IGameObject gameObject = FindNearestUnvisitedObject(); IPlayerCharacter localPlayer2 = objectTable.LocalPlayer; if (gameObject != null && localPlayer2 != null && Vector3.Distance(localPlayer2.Position, gameObject.Position) <= 5f) { _visitedPositions.Add(gameObject.Position); _state = State.UsingAction; } else { logger.LogDebug("Target no longer valid at destination, finding next"); TryFindAndMove(); } return ETaskResult.StillRunning; } case State.UsingAction: if (gameFunctions.UseAction(base.Task.Action)) { logger.LogDebug("Used action {Action}", base.Task.Action); _waitUntil = Environment.TickCount64 + 1000; _state = State.WaitingAfterAction; } return ETaskResult.StillRunning; case State.WaitingAfterAction: if (Environment.TickCount64 < _waitUntil) { return ETaskResult.StillRunning; } TryFindAndMove(); return ETaskResult.StillRunning; case State.MovingToWaypoint: if (movementController.IsPathfinding || movementController.IsPathRunning) { return ETaskResult.StillRunning; } if (!movementController.HasBeenMovingForAtLeast(2000)) { return ETaskResult.StillRunning; } logger.LogDebug("Reached waypoint, waiting for zone transition"); _state = State.WaitingForTransition; _lastPlayerPosition = objectTable.LocalPlayer?.Position ?? Vector3.Zero; _waypointReachedAt = Environment.TickCount64; return ETaskResult.StillRunning; case State.WaitingForTransition: { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null) { float num = Vector3.Distance(_lastPlayerPosition, localPlayer.Position); if (num > 50f) { logger.LogDebug("Zone transition detected (moved {Distance}), resuming plant clearing", num); ResetSearchState(); _state = State.FindingObject; return ETaskResult.StillRunning; } } if (FindNearestUnvisitedObject() != null) { logger.LogDebug("New objects detected, resuming plant clearing"); ResetSearchState(); TryFindAndMove(); return ETaskResult.StillRunning; } long? waypointReachedAt = _waypointReachedAt; if (waypointReachedAt.HasValue) { long valueOrDefault = waypointReachedAt.GetValueOrDefault(); if (Environment.TickCount64 > valueOrDefault + 3000) { logger.LogDebug("No transition at this waypoint, trying next"); NavigateToNextWaypoint(); } } return ETaskResult.StillRunning; } default: return ETaskResult.TaskComplete; } } private void ResetSearchState() { _visitedPositions.Clear(); _triedWaypointIndices.Clear(); _noObjectFoundSince = null; } public override bool ShouldInterruptOnDamage() { return false; } } }