220 lines
7.2 KiB
C#
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;
|
|
}
|
|
}
|