diff --git a/Daybreak/Configuration/Options/PathfindingOptions.cs b/Daybreak/Configuration/Options/PathfindingOptions.cs index 76461c6e..dc94a7fe 100644 --- a/Daybreak/Configuration/Options/PathfindingOptions.cs +++ b/Daybreak/Configuration/Options/PathfindingOptions.cs @@ -10,15 +10,7 @@ internal sealed class PathfindingOptions [OptionName(Name = "Enable Pathfinding", Description = "If true, the pathfinder will attempt to produce paths from the player position to objectives")] public bool EnablePathfinding { get; set; } = true; - [JsonProperty(nameof(ImprovedPathfinding))] - [OptionName(Name = "Distance-Based Pathfinding", Description = "If true, the pathfinder will attempt use an improved algorithm to find paths. Slower but generally more precise")] - public bool ImprovedPathfinding { get; set; } = false; - - [JsonProperty(nameof(OptimizePaths))] - [OptionName(Name = "Path Optimization", Description = "If true, the pathfinder will attempt to optimize paths. Slower but generally more precise")] - public bool OptimizePaths { get; set; } = true; - - [JsonProperty(nameof(UseCaching))] - [OptionName(Name = "Use Caching", Description = "If true, the pathfinder will use more memory to cache parts of the pathfinding algorithm")] - public bool UseCaching { get; set; } = true; + [JsonProperty(nameof(HighSensitivity))] + [OptionName(Name = "High Sensitivity", Description = "If true, the pathfinder generate much more accurate paths. This will greatly increase memory usage and map load times")] + public bool HighSensitivity { get; set; } = false; } diff --git a/Daybreak/Configuration/ProjectConfiguration.cs b/Daybreak/Configuration/ProjectConfiguration.cs index 31e7a5ac..38da2871 100644 --- a/Daybreak/Configuration/ProjectConfiguration.cs +++ b/Daybreak/Configuration/ProjectConfiguration.cs @@ -228,13 +228,13 @@ public override void RegisterServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); services.AddScoped(); services.AddScoped(sp => sp.GetRequiredService().As()!); services.AddScoped, TradeChatService>(); diff --git a/Daybreak/Daybreak.csproj b/Daybreak/Daybreak.csproj index 04f54e8e..0d199900 100644 --- a/Daybreak/Daybreak.csproj +++ b/Daybreak/Daybreak.csproj @@ -12,7 +12,7 @@ preview Daybreak.ico true - 0.9.8.156 + 0.9.8.157 true cfb2a489-db80-448d-a969-80270f314c46 True @@ -104,6 +104,7 @@ + diff --git a/Daybreak/Models/Guildwars/PathingData.cs b/Daybreak/Models/Guildwars/PathingData.cs index 7b8c2e23..90c1f294 100644 --- a/Daybreak/Models/Guildwars/PathingData.cs +++ b/Daybreak/Models/Guildwars/PathingData.cs @@ -1,12 +1,12 @@ -using System.Collections.Generic; +using SharpNav; +using System.Collections.Generic; namespace Daybreak.Models.Guildwars; public sealed class PathingData { + public NavMesh? NavMesh { get; init; } public List Trapezoids { get; init; } = []; - public List> ComputedPathingMaps { get; init; } = []; public List> OriginalPathingMaps { get; init; } = []; public List> OriginalAdjacencyList { get; init; } = []; - public List> ComputedAdjacencyList { get; init; } = []; } diff --git a/Daybreak/Services/Drawing/DrawingService.cs b/Daybreak/Services/Drawing/DrawingService.cs index 36634c3f..727a3be1 100644 --- a/Daybreak/Services/Drawing/DrawingService.cs +++ b/Daybreak/Services/Drawing/DrawingService.cs @@ -215,7 +215,7 @@ public void DrawPaths(WriteableBitmap bitmap, PathfindingCache? pathfindingCache direction.Normalize(); var increment = direction * (this.positionRadius + this.positionRadius); - while (currentPosVector.X != endPosition.X && currentPosVector.Y != endPosition.Y) + while (currentPosVector.X != endPosition.X || currentPosVector.Y != endPosition.Y) { var remaining = endVector - currentPosVector; if (remaining.LengthSquared < increment.LengthSquared) diff --git a/Daybreak/Services/Pathfinding/IPathfinder.cs b/Daybreak/Services/Pathfinding/IPathfinder.cs index ef727a87..ead49d6f 100644 --- a/Daybreak/Services/Pathfinding/IPathfinder.cs +++ b/Daybreak/Services/Pathfinding/IPathfinder.cs @@ -1,5 +1,7 @@ using Daybreak.Models.Guildwars; using Daybreak.Services.Pathfinding.Models; +using SharpNav; +using System.Collections.Generic; using System.Extensions; using System.Threading; using System.Threading.Tasks; @@ -9,5 +11,6 @@ namespace Daybreak.Services.Pathfinding; public interface IPathfinder { + Task GenerateNavMesh(List trapezoids, CancellationToken cancellationToken); Task> CalculatePath(PathingData map, Point startPoint, Point endPoint, CancellationToken cancellationToken); } diff --git a/Daybreak/Services/Pathfinding/SharpNavPathfinder.cs b/Daybreak/Services/Pathfinding/SharpNavPathfinder.cs new file mode 100644 index 00000000..229080ad --- /dev/null +++ b/Daybreak/Services/Pathfinding/SharpNavPathfinder.cs @@ -0,0 +1,171 @@ +using Daybreak.Configuration.Options; +using Daybreak.Models.Guildwars; +using Daybreak.Services.Metrics; +using Daybreak.Services.Pathfinding.Models; +using Microsoft.Extensions.Logging; +using SharpNav; +using SharpNav.Geometry; +using System; +using System.Collections.Generic; +using System.Configuration; +using System.Core.Extensions; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Extensions; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using System.Windows; + +namespace Daybreak.Services.Pathfinding; +internal sealed class SharpNavPathfinder : IPathfinder +{ + public const double MaxSensitivity = 100d; + public const double MinSensitivity = 1d; + + private const string PathfindingLatencyMetricName = "SharpNav Pathfinding Latency"; + private const string PathfindingLatencyMetricUnit = "Milliseconds"; + private const string PathfindingLatencyMetricDescription = "Amount of milliseconds elapsed while running the pathfinding algorithm. P95 aggregation"; + private const string MeshGenerationLatencyMetricName = "Pathfinding Mesh Generation Latency"; + private const string MeshGenerationLatencyMetricUnit = "Milliseconds"; + private const string MeshGenerationLatencyMetricDescription = "Amount of milliseconds elapsed while generating the pathfinding mesh. P95 aggregation"; + + private readonly Histogram meshLatencyMetric; + private readonly Histogram pathfindingLatencyMetric; + private readonly ILiveOptions liveOptions; + private readonly ILogger logger; + + public SharpNavPathfinder( + IMetricsService metricsService, + ILiveOptions liveOptions, + ILogger logger) + { + this.pathfindingLatencyMetric = metricsService.ThrowIfNull().CreateHistogram(PathfindingLatencyMetricName, PathfindingLatencyMetricUnit, PathfindingLatencyMetricDescription, Daybreak.Models.Metrics.AggregationTypes.P95); + this.meshLatencyMetric = metricsService.ThrowIfNull().CreateHistogram(MeshGenerationLatencyMetricName, MeshGenerationLatencyMetricUnit, MeshGenerationLatencyMetricDescription, Daybreak.Models.Metrics.AggregationTypes.P95); + this.liveOptions = liveOptions.ThrowIfNull(); + this.logger = logger.ThrowIfNull(); + } + + public async Task> CalculatePath(PathingData map, Point startPoint, Point endPoint, CancellationToken cancellationToken) + { + return await new TaskFactory().StartNew(() => this.CalculatePathInternal(map, startPoint, endPoint, cancellationToken), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } + + public Task GenerateNavMesh(List trapezoids, CancellationToken cancellationToken) + { + /* + * High sensitivity loads meshes in 2 - 10s. Low sensitivity generates in ~100 ms. Low sensitivity ignores small objects on the mesh. + * High sensitivity increases memory usage exponentially. On a large map, high sensitvity uses 200mbs or so of RAM, while low sensitivity uses < 10 mbs. + */ + var highSensitivity = this.liveOptions.Value.HighSensitivity; + var settings = NavMeshGenerationSettings.Default; + settings.CellSize = highSensitivity ? 60 : 200; + settings.CellHeight = highSensitivity ? 60 : 200; + settings.ContourFlags = ContourBuildFlags.None; + settings.SampleDistance = highSensitivity ? 15 : 100; + return new TaskFactory().StartNew(() => this.GenerateNavMesh(ConvertTrapezoidsToTriangles(trapezoids), settings), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); + } + + private Result CalculatePathInternal(PathingData pathingData, Point startPoint, Point endPoint, CancellationToken cancellationToken) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.CalculatePath), string.Empty); + if (pathingData is null || + pathingData.NavMesh is null) + { + scopedLogger.LogError("Null pathfinding map"); + return new PathfindingFailure.UnexpectedFailure(); + } + + var sw = Stopwatch.StartNew(); + var query = new NavMeshQuery(pathingData.NavMesh, 2048); + var startVec = new Vector3((float)startPoint.X, 0, (float)startPoint.Y); + var endVec = new Vector3((float)endPoint.X, 0, (float)endPoint.Y); + var extents = Vector3.One; + var startPolyRef = query.FindNearestPoly(ref startVec, ref extents, out var nearestStartRef, out var nearestStartPt); + var endPolyRef = query.FindNearestPoly(ref endVec, ref extents, out var nearestEndRef, out var nearestEndPt); + var path = new List(2048); + query.FindPath(nearestStartRef, nearestEndRef, ref nearestStartPt, ref nearestEndPt, path); + if (path.Count == 0) + { + scopedLogger.LogError("Unable to find path"); + return new PathfindingFailure.NoPathFound(); + } + + var straightPath = new Vector3[path.Count * 2]; + var straightPathFlags = new int[path.Count * 2]; + var straightPathRefs = new int[path.Count * 2]; + var straightPathCount = 0; + query.FindStraightPath(nearestStartPt, nearestEndPt, [.. path], path.Count, straightPath, straightPathFlags, straightPathRefs, ref straightPathCount, path.Count * 2, 0); + if (straightPathCount == 0) + { + scopedLogger.LogError("Unable to find straight path"); + return new PathfindingFailure.NoPathFound(); + } + + var pathSegments = new List(); + for (var i = 1; i < straightPathCount; i++) + { + pathSegments.Add(new PathSegment + { + StartPoint = new Point(straightPath[i - 1].X, straightPath[i - 1].Z), + EndPoint = new Point(straightPath[i].X, straightPath[i].Z) + }); + } + + this.pathfindingLatencyMetric.Record(sw.ElapsedMilliseconds); + return new PathfindingResponse + { + Pathing = pathSegments + }; + } + + private NavMesh? GenerateNavMesh(IEnumerable triangles, NavMeshGenerationSettings settings) + { + var scopedLogger = this.logger.CreateScopedLogger(nameof(this.GenerateNavMesh), string.Empty); + var sw = Stopwatch.StartNew(); + try + { + BBox3 bounds = triangles.GetBoundingBox(settings.CellSize); + var heightfield = new Heightfield(bounds, settings); + + heightfield.RasterizeTriangles(triangles); + + var compactHeightfield = new CompactHeightfield(heightfield, settings); + + compactHeightfield.Erode(settings.VoxelAgentWidth); + compactHeightfield.BuildDistanceField(); + compactHeightfield.BuildRegions(2, settings.MinRegionSize, settings.MergedRegionSize); + + var contourSet = new ContourSet(compactHeightfield, settings); + + var polyMesh = new PolyMesh(contourSet, settings); + var polyMeshDetail = new PolyMeshDetail(polyMesh, compactHeightfield, settings); + + var buildData = new NavMeshBuilder(polyMesh, polyMeshDetail, new SharpNav.Pathfinding.OffMeshConnection[0], settings); + + var navMesh = new NavMesh(buildData); + this.meshLatencyMetric.Record(sw.ElapsedMilliseconds); + return navMesh; + } + catch(Exception ex) + { + scopedLogger.LogError(ex, "Encountered exception"); + return default; + } + } + + private static IEnumerable ConvertTrapezoidsToTriangles(IEnumerable? trapezoids) + { + foreach (var trap in trapezoids ?? Enumerable.Empty()) + { + var v1 = new Vector3(trap.XTL, 0, trap.YT); // Top-left + var v2 = new Vector3(trap.XTR, 0, trap.YT); // Top-right + var v3 = new Vector3(trap.XBR, 0, trap.YB); // Bottom-right + var v4 = new Vector3(trap.XBL, 0, trap.YB); // Bottom-left + + // Create two triangles from the trapezoid + yield return new Triangle3(v1, v2, v3); + yield return new Triangle3(v1, v3, v4); + } + } +} diff --git a/Daybreak/Services/Pathfinding/StupidPathfinder.cs b/Daybreak/Services/Pathfinding/StupidPathfinder.cs deleted file mode 100644 index 43fb9045..00000000 --- a/Daybreak/Services/Pathfinding/StupidPathfinder.cs +++ /dev/null @@ -1,490 +0,0 @@ -using Daybreak.Configuration.Options; -using Daybreak.Models.Guildwars; -using Daybreak.Services.Metrics; -using Daybreak.Services.Pathfinding.Models; -using Daybreak.Utils; -using Microsoft.Extensions.Logging; -using System; -using System.Collections.Generic; -using System.Configuration; -using System.Core.Extensions; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Extensions; -using System.Linq; -using System.Logging; -using System.Threading; -using System.Threading.Tasks; -using System.Windows; - -namespace Daybreak.Services.Pathfinding; - -/// -/// Pathfinder based on Euclidean distance with discrete space. -/// -internal sealed class StupidPathfinder : IPathfinder -{ - private const string PathfindingLatencyMetricName = "Pathfinding Latency"; - private const string PathfindingLatencyMetricUnit = "Milliseconds"; - private const string PathfindingLatencyMetricDescription = "Amount of milliseconds elapsed while running the pathfinding algorithm. P95 aggregation"; - private const string OptimizationLatencyMetricName = "Pathfinding Optimization Latency"; - private const string OptimizationLatencyMetricUnit = "Milliseconds"; - private const string OptimizationLatencyMetricDescription = "Amount of milliseconds elapsed while running the pathfinding optimization algorithm. P95 aggregation"; - private const double PathStep = 1; - - private readonly Histogram pathFindingLatencyMetric; - private readonly Histogram optimizationLatencyMetric; - private readonly ILiveOptions liveOptions; - private readonly ILogger logger; - - public StupidPathfinder( - IMetricsService metricsService, - ILiveOptions liveOptions, - ILogger logger) - { - this.liveOptions = liveOptions.ThrowIfNull(); - this.logger = logger.ThrowIfNull(); - this.pathFindingLatencyMetric = metricsService.ThrowIfNull().CreateHistogram(PathfindingLatencyMetricName, PathfindingLatencyMetricUnit, PathfindingLatencyMetricDescription, Daybreak.Models.Metrics.AggregationTypes.P95); - this.optimizationLatencyMetric = metricsService.ThrowIfNull().CreateHistogram(OptimizationLatencyMetricName, OptimizationLatencyMetricUnit, OptimizationLatencyMetricDescription, Daybreak.Models.Metrics.AggregationTypes.P95); - } - - public Task> CalculatePath(PathingData map, Point startPoint, Point endPoint, CancellationToken cancellationToken) - { - return Task.Factory.StartNew(() => - { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.CalculatePath), string.Empty); - try - { - var sw = Stopwatch.StartNew(); - var result = this.CalculatePathInternal(map, startPoint, endPoint); - var ms = sw.ElapsedMilliseconds; - this.pathFindingLatencyMetric.Record(ms); - return result; - } - catch(Exception e) - { - scopedLogger.LogError(e, "Encountered exception during pathfinding"); - } - - return new PathfindingFailure.UnexpectedFailure(); - }, cancellationToken); - } - - private Result CalculatePathInternal(PathingData map, Point startPoint, Point endPoint) - { - var scopedLogger = this.logger.CreateScopedLogger(nameof(this.CalculatePath), string.Empty); - if (this.liveOptions.Value.EnablePathfinding is false) - { - scopedLogger.LogInformation("Pathfinding is disabled"); - return new PathfindingFailure.PathfindingDisabled(); - } - - if (map is null || - map.Trapezoids is null) - { - scopedLogger.LogError("Null pathfinding map"); - return new PathfindingFailure.UnexpectedFailure(); - } - - if (GetContainingTrapezoid(map, startPoint) is not Trapezoid startTrapezoid) - { - scopedLogger.LogInformation("Start point not in map. Getting closest start point in map"); - (var maybeClosestPoint, var maybeClosestTrapezoid) = GetClosestTrapezoidAndInnerPointToPoint(map.Trapezoids, startPoint); - if (maybeClosestPoint is not Point newStartPoint || - maybeClosestTrapezoid is not Trapezoid newStartTrapezoid) - { - scopedLogger.LogError("Unable to find closest start point in map"); - return new PathfindingFailure.UnexpectedFailure(); - } - - startPoint = newStartPoint; - startTrapezoid = newStartTrapezoid; - } - - if (GetContainingTrapezoid(map, endPoint) is not Trapezoid endTrapezoid) - { - scopedLogger.LogInformation("End point not in map. Getting closest end point in map"); - (var maybeClosestPoint, var maybeClosestTrapezoid) = GetClosestTrapezoidAndInnerPointToPoint(map.Trapezoids, endPoint); - if (maybeClosestPoint is not Point newEndPoint || - maybeClosestTrapezoid is not Trapezoid newEndTrapezoid) - { - scopedLogger.LogError("Unable to find closest end point in map"); - return new PathfindingFailure.UnexpectedFailure(); - } - - endPoint = newEndPoint; - endTrapezoid = newEndTrapezoid; - } - - /* - * First generate a list of trapezoids that should contain the final path - */ - if (this.GetTrapezoidPath(map, startTrapezoid, endTrapezoid, startPoint) is not List pathList) - { - scopedLogger.LogInformation("Unable to find an initial path"); - return new PathfindingFailure.NoPathFound(); - } - - /* - * Generate a list of points along the trapezoid path - */ - var pathfinding = new List(); - var currentPoint = startPoint; - var currentDirection = endPoint - currentPoint; - currentDirection.Normalize(); - for (var i = 0; i < pathList.Count - 1; i++) - { - var currentTrapezoid = map.Trapezoids[pathList[i]]; - var nextTrapezoid = map.Trapezoids[pathList[i + 1]]; - var currentTrajectoryEndPoint = new Point(currentDirection.X * 10e6, currentDirection.Y * 10e6); - if (LineSegmentIntersectsTrapezoid(currentPoint, currentTrajectoryEndPoint, currentTrapezoid) is not Point intersectionPoint) - { - scopedLogger.LogInformation("Unable to find an optimal path"); - return new PathfindingFailure.NoPathFound(); - } - - var validPoint = false; - intersectionPoint += currentDirection * PathStep; - for (var j = i + 1; j < pathList.Count; j++) - { - var subsequentTrapezoid = map.Trapezoids[pathList[j]]; - if (MathUtils.PointInsideTrapezoid(subsequentTrapezoid, intersectionPoint)) - { - validPoint = true; - i = j - 1; - break; - } - } - - if (!validPoint) - { - var newCurrentPoint = GetClosestPointInTrapezoid(intersectionPoint, nextTrapezoid); - currentDirection = endPoint - newCurrentPoint; - currentDirection.Normalize(); - newCurrentPoint = new Point(newCurrentPoint.X, newCurrentPoint.Y); - pathfinding.Add(new PathSegment { StartPoint = currentPoint, EndPoint = newCurrentPoint }); - currentPoint = newCurrentPoint; - } - else - { - var newCurrentPoint = intersectionPoint; - pathfinding.Add(new PathSegment { StartPoint = currentPoint, EndPoint = newCurrentPoint }); - currentPoint = newCurrentPoint; - } - } - - pathfinding.Add(new PathSegment { StartPoint = currentPoint, EndPoint = endPoint }); - pathfinding = this.OptimizePath(map, pathfinding, scopedLogger); - return new PathfindingResponse - { - Pathing = pathfinding - }; - } - - private List OptimizePath(PathingData map, List pathSegments, ScopedLogger scopedLogger) - { - if (!this.liveOptions.Value.OptimizePaths) - { - scopedLogger.LogInformation("Skipping path optimization due to performance issues"); - return pathSegments; - } - - var sw = Stopwatch.StartNew(); - /* - * Optimize the final path by excluding redundant points. - * Remove points to generate new paths, then walk these new paths, checking discrete points - * that they are inside trapezoids. - */ - var passes = 0; - var nodesRemoved = 0; - bool changed; - do - { - changed = false; - var increment = Math.Max(pathSegments.Count / 100, 1); - for (var i = 0; i < pathSegments.Count - 1; i += increment) - { - var firstSegment = pathSegments[i]; - var secondSegment = pathSegments[i + 1]; - var newSegmentStart = firstSegment.StartPoint; - var newSegmentEnd = secondSegment.EndPoint; - var direction = newSegmentEnd - newSegmentStart; - direction.Normalize(); - - var valid = true; - var newCurrentPoint = newSegmentStart; - - /* - * To improve performance, cache the current trapezoid. Very rarely the path crosses across multiple trapezoids. This way - */ - var maybeTrapezoid = GetContainingTrapezoid(map, newCurrentPoint); - while ((newSegmentEnd - newCurrentPoint).LengthSquared > 10000) - { - newCurrentPoint += direction * 100; - if (maybeTrapezoid is Trapezoid trapezoid && - !MathUtils.PointInsideTrapezoid(trapezoid, newCurrentPoint)) - { - /* - * If the current point is outside of the trapezoid we were traversing, try to find the new trapezoid it traverses. - * First check the neighbors for the current trapezoid, then expand the search to all trapezoids - */ - maybeTrapezoid = FindTrapezoidContainingPointFromNeighbors(map, trapezoid, newCurrentPoint); - if (maybeTrapezoid is null) - { - maybeTrapezoid = GetContainingTrapezoid(map, newCurrentPoint); - } - } - - if (maybeTrapezoid is null) - { - valid = false; - break; - } - } - - if (!valid) - { - continue; - } - - pathSegments.Remove(firstSegment); - pathSegments.Remove(secondSegment); - nodesRemoved++; - pathSegments.Insert(i, new PathSegment { StartPoint = newSegmentStart, EndPoint = newSegmentEnd }); - i -= increment; // Stay on the same position to try and further optimize the current path segment - changed = true; - } - - passes++; - } while (changed); - - scopedLogger.LogInformation($"Optimized path after {passes} passes. Removed {nodesRemoved} nodes"); - this.optimizationLatencyMetric.Record(sw.ElapsedMilliseconds); - return pathSegments; - } - - private static Trapezoid? GetContainingTrapezoid(PathingData map, Point point) - { - if (map is null || - map.Trapezoids is null) - { - return default; - } - - foreach(var trapezoid in map.Trapezoids) - { - if (MathUtils.PointInsideTrapezoid(trapezoid, point)) - { - return trapezoid; - } - } - - return default; - } - - private List? GetTrapezoidPath(PathingData map, Trapezoid startTrapezoid, Trapezoid endTrapezoid, Point startPoint) - { - if (this.liveOptions.Value.ImprovedPathfinding) - { - return GetTrapezoidPath2(map, startTrapezoid, endTrapezoid, startPoint); - } - else - { - return GetTrapezoidPath1(map, startTrapezoid, endTrapezoid); - } - } - - private static List? GetTrapezoidPath1(PathingData map, Trapezoid startTrapezoid, Trapezoid endTrapezoid) - { - var found = false; - var visited = new int[map.Trapezoids.Count]; - var visitationQueue = new Queue(); - visitationQueue.Enqueue(startTrapezoid); - visited[startTrapezoid.Id] = (int)startTrapezoid.Id + 1; - - while (visitationQueue.TryDequeue(out var currentTrapezoid)) - { - if (currentTrapezoid.Id == endTrapezoid.Id) - { - found = true; - break; - } - - foreach (var adjacentTrapezoidId in map.ComputedAdjacencyList[currentTrapezoid.Id]) - { - var nextTrapezoid = map.Trapezoids[adjacentTrapezoidId]; - if (visited[adjacentTrapezoidId] > 0) - { - continue; - } - - visited[adjacentTrapezoidId] = currentTrapezoid.Id + 1; - visitationQueue.Enqueue(nextTrapezoid); - } - } - - if (!found) - { - return default; - } - - var backTrackingList = new List(); - var currentTrapezoidId = (int)endTrapezoid.Id; - while (true) - { - backTrackingList.Add(currentTrapezoidId); - if (currentTrapezoidId == startTrapezoid.Id) - { - backTrackingList.Reverse(); - return backTrackingList; - } - - currentTrapezoidId = visited[currentTrapezoidId] - 1; - } - } - - private static List? GetTrapezoidPath2(PathingData map, Trapezoid startTrapezoid, Trapezoid endTrapezoid, Point startPoint) - { - var found = false; - var visited = new int[map.Trapezoids.Count]; - var distances = new double[map.Trapezoids.Count]; - var minFoundDistance = double.MaxValue; - foreach(var trapezoid in map.Trapezoids) - { - distances[trapezoid.Id] = double.MaxValue; - } - - var visitationQueue = new Queue<(Trapezoid CurrentTrapezoid, double CurrentDistance, Point PreviousPoint, int PreviousTrapezoidId)>(); - foreach(var adjacentTrapezoidId in map.ComputedAdjacencyList[startTrapezoid.Id]) - { - var nextTrapezoid = map.Trapezoids[adjacentTrapezoidId]; - visitationQueue.Enqueue((nextTrapezoid, 0, startPoint, startTrapezoid.Id)); - visited[nextTrapezoid.Id] = (int)startTrapezoid.Id + 1; - } - - while(visitationQueue.TryDequeue(out var tuple)) - { - var currentTrapezoid = tuple.CurrentTrapezoid; - var currentDistance = tuple.CurrentDistance; - var previousPoint = tuple.PreviousPoint; - var previousTrapezoidId = tuple.PreviousTrapezoidId; - var closestCurrentPoint = GetClosestPointInTrapezoid(previousPoint, currentTrapezoid); - var prevToCurrentDistance = (closestCurrentPoint - previousPoint).LengthSquared; - if (distances[currentTrapezoid.Id] <= currentDistance + prevToCurrentDistance) - { - continue; - } - - currentDistance += prevToCurrentDistance; - if (minFoundDistance < currentDistance) - { - continue; - } - - distances[currentTrapezoid.Id] = currentDistance; - visited[currentTrapezoid.Id] = previousTrapezoidId + 1; - if (currentTrapezoid.Id == endTrapezoid.Id) - { - found = true; - minFoundDistance = currentDistance; - continue; - } - - foreach (var adjacentTrapezoidId in map.ComputedAdjacencyList[currentTrapezoid.Id]) - { - var nextTrapezoid = map.Trapezoids[adjacentTrapezoidId]; - visitationQueue.Enqueue((nextTrapezoid, currentDistance, closestCurrentPoint, currentTrapezoid.Id)); - } - } - - if (!found) - { - return default; - } - - var backTrackingList = new List(); - var currentTrapezoidId = (int)endTrapezoid.Id; - while (true) - { - backTrackingList.Add(currentTrapezoidId); - if (currentTrapezoidId == startTrapezoid.Id) - { - backTrackingList.Reverse(); - return backTrackingList; - } - - currentTrapezoidId = visited[currentTrapezoidId] - 1; - } - } - - private static Point GetClosestPointInTrapezoid(Point startingPoint, Trapezoid destination) - { - var trapezoidPoints = MathUtils.GetTrapezoidPoints(destination); - - var closestPoint = new Point(); - var closestDistance = double.MaxValue; - for(var i = 0; i < trapezoidPoints.Length - 1; i++) - { - var closestPointToSegment = MathUtils.ClosestPointOnLineSegment(trapezoidPoints[i], trapezoidPoints[i + 1], startingPoint); - var distanceToSegment = (startingPoint - closestPointToSegment).LengthSquared; - if (distanceToSegment < closestDistance && - distanceToSegment > 0) - { - closestDistance = distanceToSegment; - closestPoint = closestPointToSegment; - } - } - - return closestPoint; - } - - private static Point? LineSegmentIntersectsTrapezoid(Point p1, Point p2, Trapezoid trapezoid) - { - var trapezoidPoints = MathUtils.GetTrapezoidPoints(trapezoid); - for(var i = 0; i < trapezoidPoints.Length; i++) - { - var p3 = trapezoidPoints[i]; - var p4 = trapezoidPoints[(i + 1) % trapezoidPoints.Length]; - if (MathUtils.LineSegmentsIntersect(p1, p2, p3, p4, out var intersectionPoint, epsilon: 0.1)) - { - return intersectionPoint; - } - } - - return default; - } - - private static Trapezoid? FindTrapezoidContainingPointFromNeighbors(PathingData map, Trapezoid currentTrapezoid, Point currentPoint) - { - var neighbors = map.ComputedAdjacencyList[currentTrapezoid.Id].Select(id => map.Trapezoids[id]); - foreach(var neighbor in neighbors) - { - if (MathUtils.PointInsideTrapezoid(neighbor, currentPoint)) - { - return neighbor; - } - } - - return default; - } - - private static (Point? ClosestPoint, Trapezoid? ClosestTrapezoid) GetClosestTrapezoidAndInnerPointToPoint(List trapezoids, Point point) - { - var distance = double.MaxValue; - var closestPoint = (Point?) null; - var closestTrapezoid = (Trapezoid?) null; - foreach(var trapezoid in trapezoids) - { - var p = GetClosestPointInTrapezoid(point, trapezoid); - var d = (p - point).LengthSquared; - if (d < distance) - { - closestPoint = p; - distance = d; - closestTrapezoid = trapezoid; - } - } - - return (closestPoint, closestTrapezoid); - } -} diff --git a/Daybreak/Services/Scanner/GWCAMemoryReader.cs b/Daybreak/Services/Scanner/GWCAMemoryReader.cs index 1d453769..d7c84782 100644 --- a/Daybreak/Services/Scanner/GWCAMemoryReader.cs +++ b/Daybreak/Services/Scanner/GWCAMemoryReader.cs @@ -15,21 +15,27 @@ using System.Windows; using Daybreak.Utils; using System.Text.RegularExpressions; +using SharpNav; +using SharpNav.Geometry; +using Daybreak.Services.Pathfinding; namespace Daybreak.Services.Scanner; public sealed class GWCAMemoryReader : IGuildwarsMemoryReader { private static readonly Regex ItemNameColorRegex = new(@"|", RegexOptions.Compiled); + private readonly IPathfinder pathfinder; private readonly IGWCAClient client; private readonly ILogger logger; private ConnectionContext? connectionContextCache; public GWCAMemoryReader( + IPathfinder pathfinder, IGWCAClient gWCAClient, ILogger logger) { + this.pathfinder = pathfinder.ThrowIfNull(); this.client = gWCAClient.ThrowIfNull(); this.logger = logger.ThrowIfNull(); } @@ -275,15 +281,12 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat originalPathingMaps[trapezoid.PathingMapId].Add(trapezoid.Id); } - var computedPathingMaps = BuildPathingMaps(trapezoidList, adjacencyList); - var computedAdjacencyList = BuildFinalAdjacencyList(trapezoidList, computedPathingMaps, adjacencyList); return new PathingData { Trapezoids = trapezoidList, OriginalAdjacencyList = adjacencyList, OriginalPathingMaps = originalPathingMaps, - ComputedAdjacencyList = computedAdjacencyList, - ComputedPathingMaps = computedPathingMaps + NavMesh = await this.pathfinder.GenerateNavMesh(trapezoidList, cancellationToken) }; } catch (Exception ex) @@ -500,7 +503,7 @@ public async Task EnsureInitialized(Process process, CancellationToken cancellat _ = Campaign.TryParse((int)mapPayload.Campaign, out var campaign); _ = Continent.TryParse((int)mapPayload.Continent, out var continent); - _ = Region.TryParse((int)mapPayload.Region, out var region); + _ = Daybreak.Models.Guildwars.Region.TryParse((int)mapPayload.Region, out var region); _ = Map.TryParse((int)mapPayload.Id, out var map); return new WorldData diff --git a/Daybreak/Services/Screenshots/OnlinePictureClient.cs b/Daybreak/Services/Screenshots/OnlinePictureClient.cs index 7bd1412f..37a82e45 100644 --- a/Daybreak/Services/Screenshots/OnlinePictureClient.cs +++ b/Daybreak/Services/Screenshots/OnlinePictureClient.cs @@ -48,7 +48,11 @@ public OnlinePictureClient( public async Task<(ImageSource? Source, string Credit)> GetImage(bool localized) { (var uri, var credit) = await this.GetImageUri(localized); - var localUri = Path.GetFullPath(Path.Combine(CacheFolder, uri)).Replace("https:\\", "").Replace("http:\\", ""); + var localUri = Path.GetFullPath(Path.Combine(CacheFolder, uri)) + .Replace("https:\\", "").Replace("http:\\", "") + .Replace("?", "") + .Replace("&", "") + .Replace("=", ""); if (!File.Exists(localUri)) { var imageStream = await this.GetRemoteImage(uri);