| | | 1 | | using FixedMathSharp; |
| | | 2 | | using GridForge.Grids; |
| | | 3 | | using GridForge.Spatial; |
| | | 4 | | using SwiftCollections; |
| | | 5 | | using System; |
| | | 6 | | using System.Linq; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Threading; |
| | | 9 | | |
| | | 10 | | namespace Trailblazer.Pathing; |
| | | 11 | | |
| | | 12 | | internal struct VolumeVoxelMeta |
| | | 13 | | { |
| | | 14 | | public int MovementCost; |
| | | 15 | | |
| | | 16 | | public WorldVoxelIndex? NextTrailIndex; |
| | | 17 | | } |
| | | 18 | | |
| | | 19 | | /// <summary> |
| | | 20 | | /// Executes chart-optional A* pathfinding through raw voxel volume. |
| | | 21 | | /// </summary> |
| | | 22 | | public sealed class VolumeSurveyor |
| | | 23 | | { |
| | 1 | 24 | | private static readonly Lazy<VolumeSurveyor> _instance = |
| | 1 | 25 | | new(() => new VolumeSurveyor(), LazyThreadSafetyMode.ExecutionAndPublication); |
| | | 26 | | |
| | | 27 | | /// <summary> |
| | | 28 | | /// Gets the singleton instance of the VolumeSurveyor class. |
| | | 29 | | /// </summary> |
| | | 30 | | /// <remarks> |
| | | 31 | | /// Use this property to access the shared VolumeSurveyor instance throughout the application. |
| | | 32 | | /// This instance is thread-safe and intended for global use. |
| | | 33 | | /// </remarks> |
| | 25 | 34 | | public static VolumeSurveyor Shared => _instance.Value; |
| | | 35 | | |
| | 965 | 36 | | private readonly SurveyorLock _scratchLock = new(); |
| | | 37 | | |
| | 965 | 38 | | private readonly PathHeap<Voxel> _heap = new(); |
| | | 39 | | |
| | | 40 | | // Key by stable voxel index so reconstruction is independent of GridForge voxel wrapper identity. |
| | 965 | 41 | | private readonly SwiftDictionary<WorldVoxelIndex, VolumeVoxelMeta> _meta = new(); |
| | | 42 | | |
| | 965 | 43 | | private readonly SwiftList<Voxel> _rawPath = new(); |
| | | 44 | | |
| | 965 | 45 | | private readonly SwiftList<AStarWaypoint> _waypoints = new(); |
| | | 46 | | |
| | 965 | 47 | | private readonly SwiftHashSet<string> _chartKeys = new(); |
| | | 48 | | |
| | | 49 | | private VolumePathRequest? _request; |
| | | 50 | | |
| | | 51 | | /// <summary> |
| | | 52 | | /// Finds a navigable path through the volume based on the specified pathfinding request. |
| | | 53 | | /// </summary> |
| | | 54 | | /// <remarks> |
| | | 55 | | /// Returns an empty result if the request is null, has zero displacement, or does not specify |
| | | 56 | | /// valid endpoints. The method is thread-safe and synchronizes access to reusable scratch state. |
| | | 57 | | /// </remarks> |
| | | 58 | | /// <param name="request">The pathfinding request that defines the start and end points, as well as any constraints |
| | | 59 | | /// path search. Cannot be null, must have valid endpoints, and must specify a non-zero displacement.</param> |
| | | 60 | | /// <returns> |
| | | 61 | | /// A VolumeSurveyResult containing the computed path and related metadata if a valid path is found; otherwise, |
| | | 62 | | /// VolumeSurveyResult.Empty. |
| | | 63 | | /// </returns> |
| | | 64 | | public VolumeSurveyResult FindPath(VolumePathRequest request) |
| | | 65 | | { |
| | 147 | 66 | | lock (_scratchLock) |
| | | 67 | | { |
| | 147 | 68 | | if (request == null || request.HasZeroDisplacement || !request.HasValidEndpoints) |
| | 3 | 69 | | return VolumeSurveyResult.Empty; |
| | | 70 | | |
| | 144 | 71 | | _request = request; |
| | 144 | 72 | | VolumePathRequest activeRequest = _request; |
| | | 73 | | |
| | 144 | 74 | | ClearWorkingState(); |
| | | 75 | | |
| | 144 | 76 | | _meta[activeRequest.StartNode!.WorldIndex] = new VolumeVoxelMeta(); |
| | 144 | 77 | | _heap.Add(activeRequest.StartNode!, 0); |
| | | 78 | | |
| | 144 | 79 | | if (!TracePath()) |
| | | 80 | | { |
| | 5 | 81 | | ClearWorkingState(); |
| | 5 | 82 | | return VolumeSurveyResult.Empty; |
| | | 83 | | } |
| | | 84 | | |
| | 139 | 85 | | BuildRawPath(); |
| | 139 | 86 | | TrackRawPathChartOwners(); |
| | 139 | 87 | | BuildWaypoints(); |
| | | 88 | | |
| | 139 | 89 | | VolumeSurveyResult result = VolumeSurveyResult.Create( |
| | 139 | 90 | | request.Context, |
| | 139 | 91 | | _waypoints.ToArray(), |
| | 139 | 92 | | _chartKeys.ToArray(), |
| | 139 | 93 | | request.RequestCacheKey); |
| | 139 | 94 | | ClearWorkingState(); |
| | 139 | 95 | | return result; |
| | | 96 | | } |
| | 147 | 97 | | } |
| | | 98 | | |
| | | 99 | | private bool TracePath() |
| | | 100 | | { |
| | 144 | 101 | | VolumePathRequest request = _request!; |
| | 144 | 102 | | int iterations = 0; |
| | 144 | 103 | | int searchSize = request.MaxPathSearchRange; |
| | | 104 | | |
| | 366 | 105 | | while (_heap.RemoveFirst(out Voxel? current) |
| | 366 | 106 | | && current != null |
| | 366 | 107 | | && iterations++ < searchSize) |
| | | 108 | | { |
| | 361 | 109 | | if (ProcessNeighbors(current)) |
| | 139 | 110 | | return true; |
| | | 111 | | |
| | 222 | 112 | | _heap.SetClosed(current); |
| | 222 | 113 | | } |
| | | 114 | | |
| | 5 | 115 | | return false; |
| | | 116 | | } |
| | | 117 | | |
| | | 118 | | private bool ProcessNeighbors(Voxel current) |
| | | 119 | | { |
| | 362 | 120 | | if (!_meta.TryGetValue(current.WorldIndex, out VolumeVoxelMeta data)) |
| | 1 | 121 | | return false; |
| | | 122 | | |
| | 361 | 123 | | if (TryProcessDirections( |
| | 361 | 124 | | current, |
| | 361 | 125 | | SpatialAwareness.PerpendicularDirections, |
| | 361 | 126 | | data.MovementCost + AStarSurveyor.StraightCost)) |
| | | 127 | | { |
| | 128 | 128 | | return true; |
| | | 129 | | } |
| | | 130 | | |
| | 233 | 131 | | if (TryProcessDirections( |
| | 233 | 132 | | current, |
| | 233 | 133 | | SpatialAwareness.DiagonalDirections, |
| | 233 | 134 | | data.MovementCost + AStarSurveyor.DiagonalCost, |
| | 233 | 135 | | checkEdges: true)) |
| | | 136 | | { |
| | 11 | 137 | | return true; |
| | | 138 | | } |
| | | 139 | | |
| | 222 | 140 | | return false; |
| | | 141 | | } |
| | | 142 | | |
| | | 143 | | private bool TryProcessDirections( |
| | | 144 | | Voxel current, |
| | | 145 | | SpatialDirection[] directions, |
| | | 146 | | int movementCost, |
| | | 147 | | bool checkEdges = false) |
| | | 148 | | { |
| | 594 | 149 | | if (!_request!.Context.World.TryGetGrid(current.GridIndex, out VoxelGrid? grid)) |
| | 0 | 150 | | return false; |
| | | 151 | | |
| | 13611 | 152 | | foreach (SpatialDirection dir in directions) |
| | | 153 | | { |
| | 6281 | 154 | | if (!current.TryGetNeighborFromDirection(grid!, dir, out Voxel? neighbor, useCache: true) |
| | 6281 | 155 | | || _heap.IsClosed(neighbor!)) |
| | | 156 | | { |
| | | 157 | | continue; |
| | | 158 | | } |
| | | 159 | | |
| | 5980 | 160 | | if (checkEdges && !HasValidDiagonalLegs(current, dir)) |
| | | 161 | | continue; |
| | | 162 | | |
| | 1836 | 163 | | if (CanTraverseVoxel(neighbor!) != true) |
| | | 164 | | continue; |
| | | 165 | | |
| | 738 | 166 | | if (ProcessNeighbor(current, neighbor!, movementCost)) |
| | 139 | 167 | | return true; |
| | | 168 | | } |
| | | 169 | | |
| | 455 | 170 | | return false; |
| | | 171 | | } |
| | | 172 | | |
| | | 173 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 174 | | private bool HasValidDiagonalLegs(Voxel current, SpatialDirection diagonal) |
| | | 175 | | { |
| | 4348 | 176 | | if (!_request!.Context.World.TryGetGrid(current.GridIndex, out VoxelGrid? grid)) |
| | 0 | 177 | | return false; |
| | | 178 | | |
| | 4348 | 179 | | (int dx, int dy, int dz) = SpatialAwareness.DirectionOffsets[(int)diagonal]; |
| | | 180 | | |
| | 4348 | 181 | | if (dx != 0 && !IsLegClear(grid!, current, DiagonalTraversalLegs.ForXOffset(dx))) |
| | 1264 | 182 | | return false; |
| | | 183 | | |
| | 3084 | 184 | | if (dy != 0 && !IsLegClear(grid!, current, DiagonalTraversalLegs.ForYOffset(dy))) |
| | 2452 | 185 | | return false; |
| | | 186 | | |
| | 632 | 187 | | if (dz != 0 && !IsLegClear(grid!, current, DiagonalTraversalLegs.ForZOffset(dz))) |
| | 428 | 188 | | return false; |
| | | 189 | | |
| | 204 | 190 | | return true; |
| | | 191 | | } |
| | | 192 | | |
| | | 193 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 194 | | private bool IsLegClear(VoxelGrid grid, Voxel current, SpatialDirection legDir) |
| | | 195 | | { |
| | 6636 | 196 | | return current.TryGetNeighborFromDirection(grid, legDir, out Voxel? leg, useCache: true) |
| | 6636 | 197 | | && CanTraverseVoxel(leg!); |
| | | 198 | | } |
| | | 199 | | |
| | | 200 | | private bool ProcessNeighbor(Voxel current, Voxel neighbor, int movementCost) |
| | | 201 | | { |
| | 739 | 202 | | VolumePathRequest request = _request!; |
| | 739 | 203 | | int totalMovementCost = movementCost + GetTraversalCostModifier(neighbor); |
| | | 204 | | |
| | 739 | 205 | | if (neighbor == request.EndNode) |
| | | 206 | | { |
| | 139 | 207 | | SetVoxelData(neighbor, current.WorldIndex, totalMovementCost); |
| | 139 | 208 | | return true; |
| | | 209 | | } |
| | | 210 | | |
| | 600 | 211 | | int pathCost = CalculatePathCost(neighbor.WorldPosition, totalMovementCost); |
| | 600 | 212 | | if (!_heap.Contains(neighbor)) |
| | | 213 | | { |
| | 479 | 214 | | SetVoxelData(neighbor, current.WorldIndex, totalMovementCost); |
| | 479 | 215 | | _heap.Add(neighbor, pathCost); |
| | | 216 | | } |
| | 121 | 217 | | else if (_meta.TryGetValue(neighbor.WorldIndex, out VolumeVoxelMeta neighborData) |
| | 121 | 218 | | && neighborData.MovementCost > totalMovementCost) |
| | | 219 | | { |
| | 1 | 220 | | SetVoxelData(neighbor, current.WorldIndex, totalMovementCost); |
| | 1 | 221 | | _heap.UpdatePathCost(neighbor, pathCost); |
| | 1 | 222 | | _heap.SortUp(neighbor); |
| | | 223 | | } |
| | | 224 | | |
| | 600 | 225 | | return false; |
| | | 226 | | } |
| | | 227 | | |
| | | 228 | | private void SetVoxelData( |
| | | 229 | | Voxel voxel, |
| | | 230 | | WorldVoxelIndex nextTrailIndex, |
| | | 231 | | int movementCost) |
| | | 232 | | { |
| | 619 | 233 | | _meta[voxel.WorldIndex] = new VolumeVoxelMeta() |
| | 619 | 234 | | { |
| | 619 | 235 | | MovementCost = movementCost, |
| | 619 | 236 | | NextTrailIndex = nextTrailIndex |
| | 619 | 237 | | }; |
| | 619 | 238 | | } |
| | | 239 | | |
| | | 240 | | private void BuildRawPath() |
| | | 241 | | { |
| | 139 | 242 | | VolumePathRequest request = _request!; |
| | 139 | 243 | | Voxel current = request.EndNode!; |
| | 494 | 244 | | while (current != request.StartNode) |
| | | 245 | | { |
| | 355 | 246 | | _rawPath.Insert(0, current); |
| | | 247 | | |
| | 355 | 248 | | if (!_meta.TryGetValue(current.WorldIndex, out VolumeVoxelMeta data) |
| | 355 | 249 | | || !data.NextTrailIndex.HasValue) |
| | | 250 | | break; |
| | | 251 | | |
| | 355 | 252 | | if (!request.Context.World.TryGetGridAndVoxel(data.NextTrailIndex.Value, out _, out Voxel? nextTrailVoxel) |
| | 355 | 253 | | || nextTrailVoxel == null) |
| | | 254 | | break; |
| | | 255 | | |
| | 355 | 256 | | current = nextTrailVoxel; |
| | | 257 | | } |
| | | 258 | | |
| | 139 | 259 | | _rawPath.Insert(0, request.StartNode!); |
| | 139 | 260 | | } |
| | | 261 | | |
| | | 262 | | private void BuildWaypoints() |
| | | 263 | | { |
| | 139 | 264 | | _waypoints.EnsureCapacity(_rawPath.Count); |
| | | 265 | | |
| | 139 | 266 | | Voxel start = _rawPath[0]; |
| | 139 | 267 | | _waypoints.Add(new AStarWaypoint() |
| | 139 | 268 | | { |
| | 139 | 269 | | Position = start.WorldPosition, |
| | 139 | 270 | | PathCost = 0, |
| | 139 | 271 | | GlobalIndex = start.WorldIndex |
| | 139 | 272 | | }); |
| | | 273 | | |
| | 139 | 274 | | Vector3d lastDirection = Vector3d.Zero; |
| | 710 | 275 | | for (int i = 1; i < _rawPath.Count - 1; i++) |
| | | 276 | | { |
| | 216 | 277 | | Vector3d direction = (_rawPath[i + 1].WorldPosition - _rawPath[i].WorldPosition).Normalize(); |
| | 216 | 278 | | if (!lastDirection.FuzzyEqual(direction)) |
| | | 279 | | { |
| | 131 | 280 | | _waypoints.Add(new AStarWaypoint() |
| | 131 | 281 | | { |
| | 131 | 282 | | Position = _rawPath[i].WorldPosition, |
| | 131 | 283 | | PathCost = GetMovementCost(_rawPath[i]), |
| | 131 | 284 | | GlobalIndex = _rawPath[i].WorldIndex |
| | 131 | 285 | | }); |
| | | 286 | | } |
| | | 287 | | |
| | 216 | 288 | | lastDirection = direction; |
| | | 289 | | } |
| | | 290 | | |
| | 139 | 291 | | Voxel end = _rawPath.FromEnd(1); |
| | 139 | 292 | | _waypoints.Add(new AStarWaypoint() |
| | 139 | 293 | | { |
| | 139 | 294 | | Position = end.WorldPosition, |
| | 139 | 295 | | PathCost = GetMovementCost(end), |
| | 139 | 296 | | GlobalIndex = end.WorldIndex, |
| | 139 | 297 | | IsGoal = true |
| | 139 | 298 | | }); |
| | 139 | 299 | | } |
| | | 300 | | |
| | | 301 | | private void TrackRawPathChartOwners() |
| | | 302 | | { |
| | 1266 | 303 | | for (int i = 0; i < _rawPath.Count; i++) |
| | 494 | 304 | | AddVoxelChartOwners(_rawPath[i]); |
| | 139 | 305 | | } |
| | | 306 | | |
| | | 307 | | private void AddVoxelChartOwners(Voxel voxel) |
| | | 308 | | { |
| | 495 | 309 | | if (voxel == null) |
| | 1 | 310 | | return; |
| | | 311 | | |
| | 494 | 312 | | if (voxel.TryGetPartition(out SolidChartPartition? pathPartition) && pathPartition != null) |
| | 101 | 313 | | ChartOwnerUtility.AddOwners(_chartKeys, pathPartition.ChartOwners); |
| | | 314 | | |
| | 494 | 315 | | if (voxel.TryGetPartition(out VolumeChartPartition? volumePartition) && volumePartition != null) |
| | 488 | 316 | | ChartOwnerUtility.AddOwners(_chartKeys, volumePartition.ChartOwners); |
| | 494 | 317 | | } |
| | | 318 | | |
| | | 319 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 320 | | private int CalculatePathCost(Vector3d currentVoxel, int movementCost) |
| | | 321 | | { |
| | 600 | 322 | | VolumePathRequest request = _request!; |
| | 600 | 323 | | return movementCost + AStarSurveyor.CalculateHeuristic( |
| | 600 | 324 | | currentVoxel, |
| | 600 | 325 | | request.EndNode!.WorldPosition, |
| | 600 | 326 | | request.Heuristic); |
| | | 327 | | } |
| | | 328 | | |
| | | 329 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 330 | | private bool CanTraverseVoxel(Voxel voxel) |
| | | 331 | | { |
| | 8472 | 332 | | VolumePathRequest request = _request!; |
| | 8472 | 333 | | if (voxel == request.StartNode) |
| | 122 | 334 | | return true; |
| | | 335 | | |
| | 8350 | 336 | | if (voxel == request.EndNode && request.AllowUnwalkableEndpoints) |
| | 16 | 337 | | return VolumeMediumRules.Matches(request.Context.Pathing.State, voxel, request.Medium); |
| | | 338 | | |
| | 8334 | 339 | | return VolumeVoxelFinder.IsTraversable( |
| | 8334 | 340 | | request.Context, |
| | 8334 | 341 | | voxel, |
| | 8334 | 342 | | request.UnitSize, |
| | 8334 | 343 | | request.Medium); |
| | | 344 | | } |
| | | 345 | | |
| | | 346 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 347 | | private int GetMovementCost(Voxel voxel) |
| | | 348 | | { |
| | 270 | 349 | | return _meta[voxel.WorldIndex].MovementCost; |
| | | 350 | | } |
| | | 351 | | |
| | | 352 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 353 | | private static int GetTraversalCostModifier(Voxel voxel) |
| | | 354 | | { |
| | 739 | 355 | | return voxel != null |
| | 739 | 356 | | && voxel.TryGetPartition(out VolumeChartPartition? volumePartition) |
| | 739 | 357 | | && volumePartition != null |
| | 739 | 358 | | ? volumePartition.PathCostModifier |
| | 739 | 359 | | : 0; |
| | | 360 | | } |
| | | 361 | | |
| | | 362 | | private void ClearWorkingState() |
| | | 363 | | { |
| | 288 | 364 | | _heap.FastClear(); |
| | 288 | 365 | | _meta.Clear(); |
| | 288 | 366 | | _rawPath.FastClear(); |
| | 288 | 367 | | _waypoints.FastClear(); |
| | 288 | 368 | | _chartKeys.Clear(); |
| | 288 | 369 | | } |
| | | 370 | | } |