using System; using System.Collections.Generic; using System.Numerics; using System.Threading; using System.Threading.Tasks; using Dalamud.Game.ClientState.Objects.SubKinds; using Dalamud.Game.ClientState.Objects.Types; using Dalamud.Plugin.Services; using FFXIVClientStructs.FFXIV.Client.System.Framework; using FFXIVClientStructs.FFXIV.Common.Component.BGCollision; using Microsoft.Extensions.Logging; using Questionable.Controller.Steps; using Questionable.External; using Questionable.Functions; using Questionable.Model; namespace Questionable.Controller.CustomDelivery; internal sealed class DeliveryNpcApproachExecutor(NavmeshIpc navmeshIpc, MovementController movementController, GameFunctions gameFunctions, IObjectTable objectTable, IClientState clientState, ILogger logger) : TaskExecutor(), IStoppableTaskExecutor, ITaskExecutor, IDisposable { private readonly List _candidates = new List(); private CancellationTokenSource? _cancellationTokenSource; private Task? _pathfindTask; private int _candidateIndex; private bool _moving; private float _interactionDistance; private Vector3 _npcPosition; protected override bool Start() { _candidates.Clear(); _candidateIndex = 0; _moving = false; _pathfindTask = null; _cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30L)); IPlayerCharacter localPlayer = objectTable.LocalPlayer; IGameObject gameObject = gameFunctions.FindObjectByDataId(base.Task.NpcDataId, null, warnIfMissing: false); _npcPosition = gameObject?.Position ?? base.Task.NpcPosition; _interactionDistance = 3f + (gameObject?.HitboxRadius ?? 0f) + (localPlayer?.HitboxRadius ?? 0f); if (localPlayer == null || clientState.TerritoryType != base.Task.TerritoryId) { return true; } if (Vector3.Distance(localPlayer.Position, _npcPosition) <= _interactionDistance + 0.5f) { return false; } float num = MathF.Atan2(localPlayer.Position.X - _npcPosition.X, localPlayer.Position.Z - _npcPosition.Z); float num2 = MathF.Max(1f, _interactionDistance - 0.5f); for (int i = 0; i < 24; i++) { int num3 = ((i != 0) ? ((i + 1) / 2 * ((i % 2 == 1) ? 1 : (-1))) : 0); float x = num + (float)Math.PI * 2f * (float)num3 / 24f; Vector3 position = new Vector3(_npcPosition.X + MathF.Sin(x) * num2, _npcPosition.Y + 5f, _npcPosition.Z + MathF.Cos(x) * num2); Vector3? pointOnFloor = navmeshIpc.GetPointOnFloor(position, unlandable: false, 0.75f); if (pointOnFloor.HasValue && Vector2.Distance(new Vector2(pointOnFloor.Value.X, pointOnFloor.Value.Z), new Vector2(_npcPosition.X, _npcPosition.Z)) <= _interactionDistance && HasLineOfSight(pointOnFloor.Value, _npcPosition) && HasBodyClearance(pointOnFloor.Value, localPlayer.HitboxRadius + 0.5f)) { _candidates.Add(pointOnFloor.Value); } } logger.LogDebug("Searching {CandidateCount} reachable approaches to delivery NPC {DataId} within {Distance:F1}y", _candidates.Count, base.Task.NpcDataId, _interactionDistance); StartNextPathfind(); return true; } public override ETaskResult Update() { if (_moving) { if (movementController.IsPathfinding || movementController.IsPathRunning) { return ETaskResult.StillRunning; } IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null && Vector3.Distance(localPlayer.Position, _npcPosition) <= _interactionDistance + 0.5f) { return ETaskResult.TaskComplete; } throw new MovementController.PathfindingFailedException("Delivery NPC approach ended outside interaction range"); } if (_pathfindTask == null) { if (_candidateIndex >= _candidates.Count) { throw new MovementController.PathfindingFailedException("No reachable approach within delivery NPC interaction range"); } StartNextPathfind(); return ETaskResult.StillRunning; } if (!_pathfindTask.IsCompleted) { return ETaskResult.StillRunning; } if (_pathfindTask.IsCanceled) { throw new MovementController.PathfindingFailedException("Delivery NPC approach search timed out"); } if (_pathfindTask.IsFaulted) { logger.LogDebug(_pathfindTask.Exception, "Delivery NPC approach candidate pathfinding failed"); _pathfindTask = null; return ETaskResult.StillRunning; } Vector3 vector = _candidates[_candidateIndex - 1]; NavPathfindResult result = _pathfindTask.Result; float num; if (!result.HasPartialInfo) { if (result.Waypoints.Count <= 0) { num = float.MaxValue; } else { List waypoints = result.Waypoints; num = Vector3.Distance(waypoints[waypoints.Count - 1], vector); } } else { num = result.EndDistance; } float num2 = num; IPlayerCharacter localPlayer2 = objectTable.LocalPlayer; if (result.Waypoints.Count > 0 && (!result.HasPartialInfo || !result.IsPartial) && num2 <= 1f && localPlayer2 != null && HasCollisionFreePath(localPlayer2.Position, result.Waypoints)) { logger.LogDebug("Selected reachable delivery NPC approach {Approach}", vector); movementController.NavigateTo(EMovementType.Quest, base.Task.NpcDataId, result.Waypoints, fly: false, sprint: true, 0.5f); _moving = true; return ETaskResult.StillRunning; } _pathfindTask = null; return ETaskResult.StillRunning; } private void StartNextPathfind() { IPlayerCharacter localPlayer = objectTable.LocalPlayer; if (localPlayer != null && _candidateIndex < _candidates.Count && _cancellationTokenSource != null) { Vector3 targetPosition = _candidates[_candidateIndex++]; _pathfindTask = navmeshIpc.Pathfind(localPlayer.Position, targetPosition, fly: false, _cancellationTokenSource.Token); } } private static bool HasLineOfSight(Vector3 from, Vector3 to) { return HasLineOfSightAtHeight(from, to, 2f); } private static bool HasBodyClearance(Vector3 position, float radius) { for (int i = 0; i < 8; i++) { float x = (float)Math.PI * 2f * (float)i / 8f; if (!HasLineOfSightAtHeight(to: new Vector3(position.X + MathF.Sin(x) * radius, position.Y, position.Z + MathF.Cos(x) * radius), from: position, height: 1f)) { return false; } } return true; } private static bool HasCollisionFreePath(Vector3 start, IReadOnlyList waypoints) { Vector3 vector = start; foreach (Vector3 waypoint in waypoints) { if (!HasLineOfSightAtHeight(vector, waypoint, 0.75f) || !HasLineOfSightAtHeight(vector, waypoint, 1.5f)) { return false; } vector = waypoint; } return true; } private unsafe static bool HasLineOfSightAtHeight(Vector3 from, Vector3 to, float height) { Framework* ptr = Framework.Instance(); if (ptr == null || ptr->BGCollisionModule == null) { return true; } from.Y += height; to.Y += height; Vector3 vector = to - from; float num = vector.Length(); if (num < 0.1f) { return true; } Vector3 vector2 = vector / num; int* flags = stackalloc int[4] { 16384, 0, 16384, 0 }; RaycastHit raycastHit = default(RaycastHit); return !ptr->BGCollisionModule->RaycastMaterialFilter(&raycastHit, &from, &vector2, num, 1, flags); } public void StopNow() { _cancellationTokenSource?.Cancel(); movementController.Stop(); } public void Dispose() { _cancellationTokenSource?.Dispose(); } public override bool ShouldInterruptOnDamage() { return false; } }