qstbak/Questionable/Questionable.Controller.CustomDelivery/DeliveryNpcApproachExecutor.cs
2026-08-19 13:19:57 +10:00

220 lines
7.2 KiB
C#

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<DeliveryNpcApproachExecutor> logger) : TaskExecutor<DeliveryNpcApproachTask>(), IStoppableTaskExecutor, ITaskExecutor, IDisposable
{
private readonly List<Vector3> _candidates = new List<Vector3>();
private CancellationTokenSource? _cancellationTokenSource;
private Task<NavPathfindResult>? _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<Vector3> 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<Vector3> 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;
}
}