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 _logger; private readonly ICallGateSubscriber<(int, int)> _apiVersion; private readonly ICallGateSubscriber _status; private readonly ICallGateSubscriber _rebuild; private readonly ICallGateSubscriber> _pathfind; private readonly ICallGateSubscriber _move; private readonly ICallGateSubscriber _stop; private readonly ICallGateSubscriber _isRunning; private readonly ICallGateSubscriber> _waypoints; private readonly ICallGateSubscriber _floorPoint; private readonly ICallGateSubscriber _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 logger) { _logger = logger; _apiVersion = pluginInterface.GetIpcSubscriber<(int, int)>("WigglyNav.ApiVersion"); _status = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Nav.Status"); _rebuild = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Nav.Rebuild"); _pathfind = pluginInterface.GetIpcSubscriber>("WigglyNav.V1.Pathfind"); _move = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Path.Move"); _stop = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Path.Stop"); _isRunning = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Path.IsRunning"); _waypoints = pluginInterface.GetIpcSubscriber>("WigglyNav.V1.Path.Waypoints"); _floorPoint = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Query.FloorPoint"); _nearestPoint = pluginInterface.GetIpcSubscriber("WigglyNav.V1.Query.NearestPoint"); } private static T Guarded(Func 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 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 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(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 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 GetWaypoints() { return Guarded(() => _waypoints.InvokeFunc()) ?? new List(); } 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(), IsPartial: false, "None", 0f); } }