qstbak/Questionable/Questionable.External/WigglyNavIpc.cs
2026-08-17 20:29:32 +10:00

193 lines
5.7 KiB
C#

using System;
using System.Collections.Generic;
using System.Numerics;
using System.Threading;
using System.Threading.Tasks;
using Dalamud.Plugin;
using Dalamud.Plugin.Ipc;
using Dalamud.Plugin.Ipc.Exceptions;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Questionable.External;
internal sealed class WigglyNavIpc
{
private readonly ILogger<WigglyNavIpc> _logger;
private readonly ICallGateSubscriber<(int, int)> _apiVersion;
private readonly ICallGateSubscriber<NavStatus> _status;
private readonly ICallGateSubscriber<bool> _rebuild;
private readonly ICallGateSubscriber<PathfindQuery, CancellationToken, Task<string>> _pathfind;
private readonly ICallGateSubscriber<MoveCommand, bool> _move;
private readonly ICallGateSubscriber<object> _stop;
private readonly ICallGateSubscriber<bool> _isRunning;
private readonly ICallGateSubscriber<List<Vector3>> _waypoints;
private readonly ICallGateSubscriber<Vector3, float, bool, Vector3?> _floorPoint;
private readonly ICallGateSubscriber<Vector3, float, Vector3?> _nearestPoint;
private int _outstandingPathfinds;
public bool IsNavmeshAvailable => TryGetApiVersion().HasValue;
public bool IsReady => GetStatus()?.Ready ?? false;
public bool IsPathRunning => Guarded(() => _isRunning.InvokeFunc(), fallback: false);
public bool IsPathfindInProgress => Volatile.Read(in _outstandingPathfinds) > 0;
public int NumQueuedPathfindRequests => Math.Max(0, Volatile.Read(in _outstandingPathfinds) - 1);
public WigglyNavIpc(IDalamudPluginInterface pluginInterface, ILogger<WigglyNavIpc> logger)
{
_logger = logger;
_apiVersion = pluginInterface.GetIpcSubscriber<(int, int)>("WigglyNav.ApiVersion");
_status = pluginInterface.GetIpcSubscriber<NavStatus>("WigglyNav.V1.Nav.Status");
_rebuild = pluginInterface.GetIpcSubscriber<bool>("WigglyNav.V1.Nav.Rebuild");
_pathfind = pluginInterface.GetIpcSubscriber<PathfindQuery, CancellationToken, Task<string>>("WigglyNav.V1.Pathfind");
_move = pluginInterface.GetIpcSubscriber<MoveCommand, bool>("WigglyNav.V1.Path.Move");
_stop = pluginInterface.GetIpcSubscriber<object>("WigglyNav.V1.Path.Stop");
_isRunning = pluginInterface.GetIpcSubscriber<bool>("WigglyNav.V1.Path.IsRunning");
_waypoints = pluginInterface.GetIpcSubscriber<List<Vector3>>("WigglyNav.V1.Path.Waypoints");
_floorPoint = pluginInterface.GetIpcSubscriber<Vector3, float, bool, Vector3?>("WigglyNav.V1.Query.FloorPoint");
_nearestPoint = pluginInterface.GetIpcSubscriber<Vector3, float, Vector3?>("WigglyNav.V1.Query.NearestPoint");
}
private static T Guarded<T>(Func<T> invoke, T fallback = default(T))
{
try
{
return invoke();
}
catch (IpcError)
{
return fallback;
}
}
public (int Major, int Minor)? TryGetApiVersion()
{
return Guarded((Func<(int, int)?>)(() => _apiVersion.InvokeFunc()), ((int, int)?)null);
}
public int GetBuildProgress()
{
NavStatus status = GetStatus();
if (status == null)
{
return 0;
}
if (status.TilesTotal > 0)
{
return Math.Clamp(status.TilesBuilt * 100 / status.TilesTotal, 0, 100);
}
if (!status.Ready)
{
return 0;
}
return 100;
}
public async Task<PathfindResult> Pathfind(Vector3 from, Vector3 to, bool fly, float tolerance, CancellationToken cancellationToken)
{
Interlocked.Increment(ref _outstandingPathfinds);
try
{
PathfindQuery query = new PathfindQuery(from, to, fly, tolerance);
Task<string> task = Guarded(() => _pathfind.InvokeFunc(query, cancellationToken));
if (task == null)
{
_logger.LogWarning("Could not pathfind via WigglyNav, the IPC gate is unavailable");
return EmptyPathfindResult();
}
string value = await task.ConfigureAwait(continueOnCapturedContext: false);
try
{
PathfindResult pathfindResult = JsonConvert.DeserializeObject<PathfindResult>(value);
if ((object)pathfindResult == null || pathfindResult.Waypoints == null)
{
_logger.LogWarning("WigglyNav pathfind answered a payload with no waypoint list, treating as no path");
return EmptyPathfindResult();
}
return pathfindResult;
}
catch (JsonException exception)
{
_logger.LogWarning(exception, "WigglyNav pathfind answered malformed JSON, treating as no path");
return EmptyPathfindResult();
}
}
finally
{
Interlocked.Decrement(ref _outstandingPathfinds);
}
}
public bool MoveTo(List<Vector3> waypoints, bool fly, float arrivalTolerance)
{
Stop();
MoveCommand command = new MoveCommand(waypoints, fly, arrivalTolerance);
return Guarded(() => _move.InvokeFunc(command), fallback: false);
}
public void Stop()
{
Guarded(delegate
{
_stop.InvokeAction();
return true;
}, fallback: false);
}
public List<Vector3> GetWaypoints()
{
return Guarded(() => _waypoints.InvokeFunc()) ?? new List<Vector3>();
}
public Vector3? GetPointOnFloor(Vector3 position, bool unlandable, float halfExtentXZ)
{
return Guarded(() => _floorPoint.InvokeFunc(position, halfExtentXZ, unlandable));
}
public Vector3? FindNearestMeshPoint(Vector3 position, float halfExtentXZ, float halfExtentY)
{
return Guarded(() => _nearestPoint.InvokeFunc(position, MathF.Max(halfExtentXZ, halfExtentY)));
}
public Vector3? FindNearestReachableMeshPoint(Vector3 position, float halfExtentXZ, float halfExtentY)
{
return FindNearestMeshPoint(position, halfExtentXZ, halfExtentY);
}
public void Reload()
{
Rebuild();
}
public void Rebuild()
{
if (!Guarded(() => _rebuild.InvokeFunc(), fallback: false))
{
_logger.LogWarning("Could not rebuild navmesh via WigglyNav, the request was refused or the gate is unavailable");
}
}
private NavStatus? GetStatus()
{
return Guarded(() => _status.InvokeFunc());
}
private static PathfindResult EmptyPathfindResult()
{
return new PathfindResult(new List<Vector3>(), IsPartial: false, "None", 0f);
}
}