| | | 1 | | using FixedMathSharp; |
| | | 2 | | using GridForge.Grids; |
| | | 3 | | using GridForge.Spatial; |
| | | 4 | | using SwiftCollections; |
| | | 5 | | using System; |
| | | 6 | | using System.Diagnostics.CodeAnalysis; |
| | | 7 | | |
| | | 8 | | namespace Trailblazer.Pathing; |
| | | 9 | | |
| | | 10 | | /// <summary> |
| | | 11 | | /// Provides context-scoped access to pooled and reusable IGuide instances for the built-in pathing strategies. |
| | | 12 | | /// Handles guide request routing, instantiation, and lifecycle management. |
| | | 13 | | /// </summary> |
| | | 14 | | internal static class PathGuideFactory |
| | | 15 | | { |
| | | 16 | | /// <summary> |
| | | 17 | | /// The number of frames after which unused guides are considered stale and eligible for eviction from the pool. |
| | | 18 | | /// This helps prevent memory bloat from guides that are rarely used but still occupy cache space. |
| | | 19 | | /// Adjust this value based on typical pathfinding usage patterns and acceptable memory overhead in your application |
| | | 20 | | /// </summary> |
| | | 21 | | private const int MaxFramesUnused = 600; |
| | | 22 | | |
| | | 23 | | /// <summary> |
| | | 24 | | /// A shared cache for A* survey results, keyed by request parameters. |
| | | 25 | | /// This allows for efficient reuse of recently computed paths without needing to re-run the A* algorithm for identi |
| | | 26 | | /// </summary> |
| | 28587 | 27 | | private static TrailblazerGuideState GuideState => PathManager.ActiveState.GuideState; |
| | | 28 | | |
| | 72 | 29 | | private static AStarSurveyor _aStarSurveyor => GuideState.AStarSurveyor; |
| | | 30 | | |
| | 30 | 31 | | private static FlowFieldSurveyor _flowFieldSurveyor => GuideState.FlowFieldSurveyor; |
| | | 32 | | |
| | 29 | 33 | | private static VolumeSurveyor _volumeSurveyor => GuideState.VolumeSurveyor; |
| | | 34 | | |
| | | 35 | | private static ReusableSurveyResultCache<AStarSurveyResult> _cachedAStarResults => |
| | 6493 | 36 | | GuideState.CachedAStarResults; |
| | | 37 | | |
| | | 38 | | /// <summary> |
| | | 39 | | /// Returns the number of active (pooled or in-use) A* results currently tracked. |
| | | 40 | | /// </summary> |
| | 3965 | 41 | | public static int TotalAStarGuideCount => _cachedAStarResults.Count; |
| | | 42 | | |
| | | 43 | | /// <summary> |
| | | 44 | | /// Returns only the number of active (in-use) A* results currently tracked. |
| | | 45 | | /// </summary> |
| | 15 | 46 | | public static int InUseAStarGuideCount => _cachedAStarResults.CountInUse; |
| | | 47 | | |
| | | 48 | | /// <summary> |
| | | 49 | | /// A shared cache for FlowField survey results, keyed by request parameters. |
| | | 50 | | /// This allows for efficient reuse of recently computed flow fields without needing to re-run the flow field genera |
| | | 51 | | /// </summary> |
| | | 52 | | private static ReusableSurveyResultCache<FlowFieldSurveyResult> _cachedFlowResults => |
| | 6299 | 53 | | GuideState.CachedFlowResults; |
| | | 54 | | |
| | | 55 | | /// <summary> |
| | | 56 | | /// Returns the number of active (pooled or in-use) FlowField guides currently tracked. |
| | | 57 | | /// </summary> |
| | 3901 | 58 | | public static int TotalFlowGuideCount => _cachedFlowResults.Count; |
| | | 59 | | |
| | | 60 | | /// <summary> |
| | | 61 | | /// Returns only the number of active (in-use) FlowField guides currently tracked. |
| | | 62 | | /// </summary> |
| | 6 | 63 | | public static int InUseFlowGuideCount => _cachedFlowResults.CountInUse; |
| | | 64 | | |
| | | 65 | | /// <summary> |
| | | 66 | | /// A shared cache for raw-volume survey results, keyed by request parameters. |
| | | 67 | | /// This allows for efficient reuse of recently computed raw-volume data without needing to re-run the volume genera |
| | | 68 | | /// </summary> |
| | | 69 | | private static ReusableSurveyResultCache<VolumeSurveyResult> _cachedVolumeResults => |
| | 7552 | 70 | | GuideState.CachedVolumeResults; |
| | | 71 | | |
| | | 72 | | /// <summary> |
| | | 73 | | /// Returns the number of active raw-volume guides currently tracked. |
| | | 74 | | /// </summary> |
| | 3905 | 75 | | public static int TotalVolumeGuideCount => _cachedVolumeResults.Count; |
| | | 76 | | |
| | | 77 | | /// <summary> |
| | | 78 | | /// Returns only the number of active (in-use) raw-volume guides currently tracked. |
| | | 79 | | /// </summary> |
| | 3 | 80 | | public static int InUseVolumeGuideCount => _cachedVolumeResults.CountInUse; |
| | | 81 | | |
| | | 82 | | private static ReusableSurveyResultCache<HybridRoutePlanSurveyResult> _cachedHybridRoutePlans => |
| | 5206 | 83 | | GuideState.CachedHybridRoutePlans; |
| | | 84 | | |
| | 1185 | 85 | | private static GuidePool<AStarGuide> _aStarGuides => GuideState.AStarGuides; |
| | | 86 | | |
| | 1112 | 87 | | private static GuidePool<FlowFieldGuide> _flowFieldGuides => GuideState.FlowFieldGuides; |
| | | 88 | | |
| | 609 | 89 | | private static GuidePool<VolumeGuide> _volumeGuides => GuideState.VolumeGuides; |
| | | 90 | | |
| | | 91 | | /// <summary> |
| | | 92 | | /// Returns the number of cached transition route plans currently tracked. |
| | | 93 | | /// </summary> |
| | 3887 | 94 | | public static int TotalHybridRoutePlanCount => _cachedHybridRoutePlans.Count; |
| | | 95 | | |
| | | 96 | | /// <summary> |
| | | 97 | | /// Returns only the number of active (in-use) hybrid route plans currently tracked. |
| | | 98 | | /// </summary> |
| | 3 | 99 | | public static int InUseHybridRoutePlanCount => _cachedHybridRoutePlans.CountInUse; |
| | | 100 | | |
| | | 101 | | /// <summary> |
| | | 102 | | /// Indicates whether any pathing guides are currently pooled and available. |
| | | 103 | | /// </summary> |
| | | 104 | | public static bool IsPooling => |
| | 3935 | 105 | | TotalAStarGuideCount > 0 |
| | 3935 | 106 | | || TotalFlowGuideCount > 0 |
| | 3935 | 107 | | || TotalVolumeGuideCount > 0 |
| | 3935 | 108 | | || TotalHybridRoutePlanCount > 0; |
| | | 109 | | |
| | | 110 | | /// <summary> |
| | | 111 | | /// Indicates whether any guides are currently in use (checked out from the pool and not yet returned). |
| | | 112 | | /// </summary> |
| | | 113 | | public static bool AnyInUse => |
| | 6 | 114 | | InUseAStarGuideCount > 0 |
| | 6 | 115 | | || InUseFlowGuideCount > 0 |
| | 6 | 116 | | || InUseVolumeGuideCount > 0 |
| | 6 | 117 | | || InUseHybridRoutePlanCount > 0; |
| | | 118 | | |
| | | 119 | | /// <summary> |
| | | 120 | | /// Attempts to remove guides from the pool that haven't been used for a configured number of frames. |
| | | 121 | | /// </summary> |
| | | 122 | | /// <param name="currentFrame">The current frame index used to check guide staleness.</param> |
| | | 123 | | public static void CullExpiredGuides(int currentFrame) |
| | | 124 | | { |
| | 4586 | 125 | | if (!IsPooling) return; |
| | | 126 | | |
| | 14 | 127 | | _cachedAStarResults.EvictStaleEntries(currentFrame, MaxFramesUnused); |
| | 14 | 128 | | _cachedFlowResults.EvictStaleEntries(currentFrame, MaxFramesUnused); |
| | 14 | 129 | | _cachedVolumeResults.EvictStaleEntries(currentFrame, MaxFramesUnused); |
| | 14 | 130 | | _cachedHybridRoutePlans.EvictStaleEntries(currentFrame, MaxFramesUnused); |
| | 14 | 131 | | } |
| | | 132 | | |
| | | 133 | | /// <summary> |
| | | 134 | | /// Requests a guide of a specific type using an already populated path request. |
| | | 135 | | /// </summary> |
| | | 136 | | /// <typeparam name="T">The concrete guide type to return.</typeparam> |
| | | 137 | | /// <param name="request">The path request with validated parameters.</param> |
| | | 138 | | /// <param name="result">The resolved guide or null if the request was invalid.</param> |
| | | 139 | | /// <returns><c>true</c> if the guide was properly configured, otherwise <c>false</c>.</returns> |
| | | 140 | | public static bool RequestGuide<T>(IPathRequest request, [NotNullWhen(true)] out T? result) where T : class, IGuide |
| | | 141 | | { |
| | 1378 | 142 | | result = default; |
| | 1378 | 143 | | if (!RequestGuide(request, out IGuide? guide)) |
| | 16 | 144 | | return false; |
| | | 145 | | |
| | 1362 | 146 | | if (guide is not T typedGuide) |
| | | 147 | | { |
| | 1 | 148 | | ReturnGuide(guide); |
| | 1 | 149 | | return false; |
| | | 150 | | } |
| | | 151 | | |
| | 1361 | 152 | | result = typedGuide; |
| | 1361 | 153 | | return true; |
| | | 154 | | } |
| | | 155 | | |
| | | 156 | | /// <summary> |
| | | 157 | | /// Routes the path request to the appropriate guide implementation based on type. |
| | | 158 | | /// </summary> |
| | | 159 | | /// <param name="request">The polymorphic request to resolve.</param> |
| | | 160 | | /// <param name="result">The resolved guide or null if the request was invalid.</param> |
| | | 161 | | /// <returns><c>true</c> if the guide was properly configured, otherwise <c>false</c>.</returns> |
| | | 162 | | public static bool RequestGuide(IPathRequest request, [NotNullWhen(true)] out IGuide? result) |
| | | 163 | | { |
| | 1447 | 164 | | if (request?.IsValid != true) |
| | | 165 | | { |
| | 2 | 166 | | TrailblazerLogger.Channel.Warn($"Request is invalid. Create or update the request before requesting a guide. |
| | 2 | 167 | | result = null; |
| | 2 | 168 | | return false; |
| | | 169 | | } |
| | | 170 | | |
| | 1445 | 171 | | if (!ReferenceEquals(request.Context, PathManager.ActiveState.Context)) |
| | | 172 | | { |
| | 1 | 173 | | result = null; |
| | 1 | 174 | | return false; |
| | | 175 | | } |
| | | 176 | | |
| | 1444 | 177 | | if (request is AStarPathRequest unreachableAStar |
| | 1444 | 178 | | && SolidPartitionReachability.IsProvablyUnreachable(unreachableAStar)) |
| | | 179 | | { |
| | 11 | 180 | | result = null; |
| | 11 | 181 | | return false; |
| | | 182 | | } |
| | | 183 | | |
| | 1433 | 184 | | result = request switch |
| | 1433 | 185 | | { |
| | 590 | 186 | | AStarPathRequest a => RequestAStar(a), |
| | 546 | 187 | | FlowFieldPathRequest f => RequestFlowField(f), |
| | 291 | 188 | | VolumePathRequest v => RequestVolume(v), |
| | 3 | 189 | | HybridPathRequest h => RequestHybrid(h), |
| | 3 | 190 | | _ => null, |
| | 1433 | 191 | | }; |
| | 1433 | 192 | | return result != null; |
| | | 193 | | } |
| | | 194 | | |
| | | 195 | | /// <summary> |
| | | 196 | | /// Retrieves an A* guide from the pool or creates a new one based on the provided request. |
| | | 197 | | /// </summary> |
| | | 198 | | /// <param name="request">The configured A* pathfinding request.</param> |
| | | 199 | | /// <returns>A valid AStarGuide instance.</returns> |
| | | 200 | | public static AStarGuide? RequestAStar(AStarPathRequest request) |
| | | 201 | | { |
| | 591 | 202 | | if (SolidPartitionReachability.IsProvablyUnreachable(request)) |
| | 1 | 203 | | return null; |
| | | 204 | | |
| | 590 | 205 | | if (_cachedAStarResults.TryCheckout(request, out AStarSurveyResult cachedResult)) |
| | 518 | 206 | | return RentAStarGuide(cachedResult); |
| | | 207 | | |
| | 72 | 208 | | return RequestAStarMiss(request); |
| | | 209 | | } |
| | | 210 | | |
| | | 211 | | private static AStarGuide? RequestAStarMiss(AStarPathRequest request) |
| | | 212 | | { |
| | 72 | 213 | | bool pathFound = _cachedAStarResults.TryGetOrCreate(request, |
| | 72 | 214 | | () => ResolveAStarResult(request), |
| | 72 | 215 | | out AStarSurveyResult result); |
| | | 216 | | |
| | 72 | 217 | | if (!pathFound) |
| | 4 | 218 | | return null; |
| | | 219 | | |
| | 68 | 220 | | return RentAStarGuide(result); |
| | | 221 | | } |
| | | 222 | | |
| | | 223 | | /// <summary> |
| | | 224 | | /// Retrieves a FlowField guide from the pool or creates a new one based on the provided request. |
| | | 225 | | /// </summary> |
| | | 226 | | /// <param name="request">The configured FlowField pathfinding request.</param> |
| | | 227 | | /// <returns>A valid FlowFieldGuide instance.</returns> |
| | | 228 | | public static FlowFieldGuide? RequestFlowField(FlowFieldPathRequest request) |
| | | 229 | | { |
| | 546 | 230 | | if (request.AllowTraversalTransitions |
| | 546 | 231 | | && TryGetCachedTransitionFallbackFlowPlan(request, out HybridRoutePlan? cachedRoutePlan)) |
| | | 232 | | { |
| | 1 | 233 | | FlowFieldGuide cachedGuide = _flowFieldGuides.Rent(); |
| | 1 | 234 | | if (cachedGuide.InitializeStaged(cachedRoutePlan)) |
| | 1 | 235 | | return cachedGuide; |
| | | 236 | | |
| | 0 | 237 | | ReturnFlowFieldGuide(cachedGuide, dispose: false); |
| | 0 | 238 | | return null; |
| | | 239 | | } |
| | | 240 | | |
| | 545 | 241 | | if (_cachedFlowResults.TryCheckout(request, out FlowFieldSurveyResult cachedResult)) |
| | | 242 | | { |
| | 515 | 243 | | if (TryRentFlowFieldGuide(request, cachedResult, out FlowFieldGuide? cachedGuide)) |
| | 514 | 244 | | return cachedGuide; |
| | | 245 | | |
| | 1 | 246 | | _cachedFlowResults.Return(cachedResult, dispose: false); |
| | | 247 | | } |
| | | 248 | | |
| | 31 | 249 | | return RequestFlowFieldMiss(request); |
| | | 250 | | } |
| | | 251 | | |
| | | 252 | | private static FlowFieldGuide? RequestFlowFieldMiss(FlowFieldPathRequest request) |
| | | 253 | | { |
| | 31 | 254 | | bool pathFound = _cachedFlowResults.TryGetOrCreate(request, |
| | 31 | 255 | | () => _flowFieldSurveyor.FindPath(request), |
| | 31 | 256 | | out FlowFieldSurveyResult result); |
| | | 257 | | |
| | | 258 | | // Make sure the start voxel is within the current fields collection. This dictionary probe is on the warm |
| | | 259 | | // cache-hit path, so GridForge's WorldVoxelIndex hash must stay allocation-free to keep FlowField hits near A*. |
| | 31 | 260 | | if (pathFound |
| | 31 | 261 | | && result.Fields != null |
| | 31 | 262 | | && request.StartNode != null |
| | 31 | 263 | | && result.Fields.ContainsKey(request.StartNode.WorldIndex)) |
| | | 264 | | { |
| | 22 | 265 | | return RentFlowFieldGuide(result); |
| | | 266 | | } |
| | | 267 | | |
| | 9 | 268 | | if (pathFound) |
| | 1 | 269 | | _cachedFlowResults.Return(result, dispose: false); |
| | | 270 | | |
| | 9 | 271 | | if (!request.AllowTraversalTransitions) |
| | 2 | 272 | | return null; |
| | | 273 | | |
| | 7 | 274 | | return TryBuildTransitionFallbackFlowGuide(request, out FlowFieldGuide? fallbackGuide) |
| | 7 | 275 | | ? fallbackGuide |
| | 7 | 276 | | : null; |
| | | 277 | | } |
| | | 278 | | |
| | | 279 | | /// <summary> |
| | | 280 | | /// Retrieves a raw-volume guide from the pool or creates a new one based on the provided request. |
| | | 281 | | /// </summary> |
| | | 282 | | public static VolumeGuide? RequestVolume(VolumePathRequest request) |
| | | 283 | | { |
| | 291 | 284 | | if (_cachedVolumeResults.TryCheckout(request, out VolumeSurveyResult cachedResult)) |
| | 262 | 285 | | return RentVolumeGuide(cachedResult); |
| | | 286 | | |
| | 29 | 287 | | return RequestVolumeMiss(request); |
| | | 288 | | } |
| | | 289 | | |
| | | 290 | | private static VolumeGuide? RequestVolumeMiss(VolumePathRequest request) |
| | | 291 | | { |
| | 29 | 292 | | bool pathFound = _cachedVolumeResults.TryGetOrCreate(request, |
| | 29 | 293 | | () => _volumeSurveyor.FindPath(request), |
| | 29 | 294 | | out VolumeSurveyResult result); |
| | | 295 | | |
| | 29 | 296 | | if (!pathFound) |
| | 1 | 297 | | return null; |
| | | 298 | | |
| | 28 | 299 | | return RentVolumeGuide(result); |
| | | 300 | | } |
| | | 301 | | |
| | | 302 | | /// <summary> |
| | | 303 | | /// Builds a hybrid guide by composing cached chart and volume segment guides from a planned route request. |
| | | 304 | | /// </summary> |
| | | 305 | | private static HybridGuide? RequestHybrid(HybridPathRequest request) |
| | | 306 | | { |
| | 3 | 307 | | HybridRoutePlan? routePlan = request.RoutePlan; |
| | 3 | 308 | | if (routePlan == null |
| | 3 | 309 | | || !HybridWaypointFlattener.TryBuild( |
| | 3 | 310 | | routePlan, |
| | 3 | 311 | | out AStarWaypoint[]? flattened, |
| | 3 | 312 | | out _)) |
| | | 313 | | { |
| | 0 | 314 | | return null; |
| | | 315 | | } |
| | | 316 | | |
| | 3 | 317 | | HybridGuide guide = new(); |
| | 3 | 318 | | return guide.Initialize(flattened!) ? guide : null; |
| | | 319 | | } |
| | | 320 | | |
| | | 321 | | /// <summary> |
| | | 322 | | /// Returns the guide back to its associated pool, optionally disposing it completely. |
| | | 323 | | /// </summary> |
| | | 324 | | /// <param name="guide">The guide to return to the cache.</param> |
| | | 325 | | /// <param name="dispose">Whether to destroy the guide instead of pooling it.</param> |
| | | 326 | | public static void ReturnGuide(IGuide? guide, bool dispose = false) |
| | | 327 | | { |
| | 1380 | 328 | | if (guide == null) return; |
| | | 329 | | |
| | 1370 | 330 | | ThrowIfGuideOwnedByDifferentContext(guide, PathManager.ActiveState.Context); |
| | | 331 | | |
| | | 332 | | switch (guide) |
| | | 333 | | { |
| | | 334 | | case AStarGuide a: |
| | 559 | 335 | | _cachedAStarResults.Return(a.TrailMap, dispose); |
| | 559 | 336 | | ReturnAStarGuide(a, dispose); |
| | 559 | 337 | | break; |
| | | 338 | | case FlowFieldGuide f: |
| | 529 | 339 | | f.ReleaseStagedResources(dispose); |
| | 529 | 340 | | if (f.FlowMap != null) |
| | 526 | 341 | | _cachedFlowResults.Return(f.FlowMap, dispose); |
| | 529 | 342 | | ReturnFlowFieldGuide(f, dispose); |
| | 529 | 343 | | break; |
| | | 344 | | case VolumeGuide v: |
| | 279 | 345 | | if (v.TrailMap != null) |
| | 279 | 346 | | _cachedVolumeResults.Return(v.TrailMap, dispose); |
| | 279 | 347 | | ReturnVolumeGuide(v, dispose); |
| | | 348 | | break; |
| | | 349 | | } |
| | 279 | 350 | | } |
| | | 351 | | |
| | | 352 | | private static void ThrowIfGuideOwnedByDifferentContext( |
| | | 353 | | IGuide guide, |
| | | 354 | | TrailblazerWorldContext expectedContext) |
| | | 355 | | { |
| | 1370 | 356 | | TrailblazerWorldContext? ownerContext = guide switch |
| | 1370 | 357 | | { |
| | 560 | 358 | | AStarGuide a => a.TrailMap.Context, |
| | 529 | 359 | | FlowFieldGuide f => f.OwnerContext, |
| | 279 | 360 | | VolumeGuide v => v.TrailMap?.Context, |
| | 2 | 361 | | _ => null, |
| | 1370 | 362 | | }; |
| | | 363 | | |
| | 1370 | 364 | | if (ownerContext == null || ReferenceEquals(ownerContext, expectedContext)) |
| | 1369 | 365 | | return; |
| | | 366 | | |
| | 1 | 367 | | throw new InvalidOperationException( |
| | 1 | 368 | | "Guide belongs to a different owning TrailblazerWorldContext. Return it through the context that created it. |
| | | 369 | | } |
| | | 370 | | |
| | | 371 | | /// <summary> |
| | | 372 | | /// Invalidates all cached results associated with the specified chart key. |
| | | 373 | | /// </summary> |
| | | 374 | | /// <remarks> |
| | | 375 | | /// Call this method to ensure that any cached data related to the specified chart is removed and |
| | | 376 | | /// will be recalculated on the next access. |
| | | 377 | | /// This is useful when the underlying chart data has changed and stale cache entries must be cleared. |
| | | 378 | | /// </remarks> |
| | | 379 | | /// <param name="chartKey">The unique key identifying the chart whose cached results should be invalidated. Cannot b |
| | | 380 | | public static void InvalidateCacheFor(string chartKey) |
| | | 381 | | { |
| | 1236 | 382 | | if (string.IsNullOrEmpty(chartKey)) return; |
| | | 383 | | |
| | 1232 | 384 | | _cachedAStarResults.InvalidateForChart(chartKey); |
| | 1232 | 385 | | _cachedFlowResults.InvalidateForChart(chartKey); |
| | 1232 | 386 | | _cachedVolumeResults.InvalidateForChart(chartKey); |
| | 1232 | 387 | | _cachedHybridRoutePlans.InvalidateForChart(chartKey); |
| | 1232 | 388 | | } |
| | | 389 | | |
| | | 390 | | internal static void InvalidateVolumeCache() |
| | | 391 | | { |
| | 1755 | 392 | | _cachedVolumeResults.InvalidateAll(); |
| | 1755 | 393 | | } |
| | | 394 | | |
| | | 395 | | /// <summary> |
| | | 396 | | /// Removes all cached A*, FlowField, and Volume guides. |
| | | 397 | | /// </summary> |
| | | 398 | | public static void FlushCache(bool force = false) |
| | | 399 | | { |
| | 42 | 400 | | if (!force && AnyInUse) return; |
| | 40 | 401 | | _cachedAStarResults.InvalidateAll(); |
| | 40 | 402 | | _cachedFlowResults.InvalidateAll(); |
| | 40 | 403 | | _cachedVolumeResults.InvalidateAll(); |
| | 40 | 404 | | _cachedHybridRoutePlans.InvalidateAll(); |
| | 40 | 405 | | ClearGuidePools(); |
| | 40 | 406 | | } |
| | | 407 | | |
| | | 408 | | internal static bool TrySeedAStarCacheForBenchmark(int requestKey, string[] chartKeys, bool checkout) |
| | | 409 | | { |
| | 5 | 410 | | TrailblazerWorldContext context = PathManager.ActiveState.Context; |
| | 5 | 411 | | return _cachedAStarResults.TrySeed( |
| | 5 | 412 | | AStarSurveyResult.Create(context, CreateSeedWaypoints(), chartKeys, requestKey), |
| | 5 | 413 | | checkout); |
| | | 414 | | } |
| | | 415 | | |
| | | 416 | | internal static bool TrySeedFlowFieldCacheForBenchmark(int requestKey, string[] chartKeys, bool checkout) |
| | | 417 | | { |
| | 1 | 418 | | WorldVoxelIndex index = default; |
| | 1 | 419 | | var fields = new SwiftDictionary<WorldVoxelIndex, FlowField>(1) |
| | 1 | 420 | | { |
| | 1 | 421 | | [index] = new FlowField |
| | 1 | 422 | | { |
| | 1 | 423 | | GlobalIndex = index, |
| | 1 | 424 | | IsGoal = true |
| | 1 | 425 | | } |
| | 1 | 426 | | }; |
| | | 427 | | |
| | 1 | 428 | | TrailblazerWorldContext context = PathManager.ActiveState.Context; |
| | 1 | 429 | | return _cachedFlowResults.TrySeed( |
| | 1 | 430 | | FlowFieldSurveyResult.Create(context, fields, chartKeys, requestKey), |
| | 1 | 431 | | checkout); |
| | | 432 | | } |
| | | 433 | | |
| | | 434 | | internal static bool TrySeedVolumeCacheForBenchmark(int requestKey, string[] chartKeys, bool checkout) |
| | | 435 | | { |
| | 3 | 436 | | TrailblazerWorldContext context = PathManager.ActiveState.Context; |
| | 3 | 437 | | return _cachedVolumeResults.TrySeed( |
| | 3 | 438 | | VolumeSurveyResult.Create(context, CreateSeedWaypoints(), chartKeys, requestKey), |
| | 3 | 439 | | checkout); |
| | | 440 | | } |
| | | 441 | | |
| | | 442 | | internal static bool TrySeedHybridRoutePlanCacheForBenchmark(int requestKey, string[] chartKeys, bool checkout) |
| | | 443 | | { |
| | 1 | 444 | | TrailblazerWorldContext context = PathManager.ActiveState.Context; |
| | 1 | 445 | | var routePlan = new HybridRoutePlan( |
| | 1 | 446 | | new[] { HybridRouteStep.Waypoint(context, Vector3d.Zero) }, |
| | 1 | 447 | | Array.Empty<TraversalTransition>(), |
| | 1 | 448 | | totalPathCost: 0); |
| | | 449 | | |
| | 1 | 450 | | return _cachedHybridRoutePlans.TrySeed( |
| | 1 | 451 | | HybridRoutePlanSurveyResult.Create(context, routePlan, chartKeys, requestKey), |
| | 1 | 452 | | checkout); |
| | | 453 | | } |
| | | 454 | | |
| | | 455 | | internal static int CountIndexedCacheEntriesForBenchmark(string chartKey) |
| | | 456 | | { |
| | 1 | 457 | | return _cachedAStarResults.CountIndexedEntriesForChart(chartKey) |
| | 1 | 458 | | + _cachedFlowResults.CountIndexedEntriesForChart(chartKey) |
| | 1 | 459 | | + _cachedVolumeResults.CountIndexedEntriesForChart(chartKey) |
| | 1 | 460 | | + _cachedHybridRoutePlans.CountIndexedEntriesForChart(chartKey); |
| | | 461 | | } |
| | | 462 | | |
| | | 463 | | private static AStarWaypoint[] CreateSeedWaypoints() |
| | | 464 | | { |
| | 8 | 465 | | return new[] |
| | 8 | 466 | | { |
| | 8 | 467 | | new AStarWaypoint |
| | 8 | 468 | | { |
| | 8 | 469 | | Position = Vector3d.Zero, |
| | 8 | 470 | | IsGoal = true |
| | 8 | 471 | | } |
| | 8 | 472 | | }; |
| | | 473 | | } |
| | | 474 | | |
| | | 475 | | private static AStarGuide? RentAStarGuide(AStarSurveyResult result) |
| | | 476 | | { |
| | 586 | 477 | | AStarGuide guide = _aStarGuides.Rent(); |
| | 586 | 478 | | if (guide.Initialize(result)) |
| | 586 | 479 | | return guide; |
| | | 480 | | |
| | 0 | 481 | | ReturnAStarGuide(guide, dispose: false); |
| | 0 | 482 | | _cachedAStarResults.Return(result, dispose: false); |
| | 0 | 483 | | return null; |
| | | 484 | | } |
| | | 485 | | |
| | | 486 | | private static FlowFieldGuide? RentFlowFieldGuide(FlowFieldSurveyResult result) |
| | | 487 | | { |
| | 536 | 488 | | FlowFieldGuide guide = _flowFieldGuides.Rent(); |
| | 536 | 489 | | if (guide.Initialize(result)) |
| | 536 | 490 | | return guide; |
| | | 491 | | |
| | 0 | 492 | | ReturnFlowFieldGuide(guide, dispose: false); |
| | 0 | 493 | | _cachedFlowResults.Return(result, dispose: false); |
| | 0 | 494 | | return null; |
| | | 495 | | } |
| | | 496 | | |
| | | 497 | | private static bool TryRentFlowFieldGuide( |
| | | 498 | | FlowFieldPathRequest request, |
| | | 499 | | FlowFieldSurveyResult result, |
| | | 500 | | [NotNullWhen(true)] out FlowFieldGuide? guide) |
| | | 501 | | { |
| | 515 | 502 | | guide = null; |
| | 515 | 503 | | if (result.Fields == null |
| | 515 | 504 | | || request.StartNode == null |
| | 515 | 505 | | || !result.Fields.ContainsKey(request.StartNode.WorldIndex)) |
| | | 506 | | { |
| | 1 | 507 | | return false; |
| | | 508 | | } |
| | | 509 | | |
| | 514 | 510 | | guide = RentFlowFieldGuide(result); |
| | 514 | 511 | | return guide != null; |
| | | 512 | | } |
| | | 513 | | |
| | | 514 | | private static VolumeGuide? RentVolumeGuide(VolumeSurveyResult result) |
| | | 515 | | { |
| | 290 | 516 | | VolumeGuide guide = _volumeGuides.Rent(); |
| | 290 | 517 | | if (guide.Initialize(result)) |
| | 290 | 518 | | return guide; |
| | | 519 | | |
| | 0 | 520 | | ReturnVolumeGuide(guide, dispose: false); |
| | 0 | 521 | | _cachedVolumeResults.Return(result, dispose: false); |
| | 0 | 522 | | return null; |
| | | 523 | | } |
| | | 524 | | |
| | | 525 | | private static void ReturnAStarGuide(AStarGuide guide, bool dispose) |
| | | 526 | | { |
| | 559 | 527 | | if (dispose || guide.GetType() != typeof(AStarGuide)) |
| | 3 | 528 | | _aStarGuides.Destroy(guide); |
| | | 529 | | else |
| | 556 | 530 | | _aStarGuides.Release(guide); |
| | 556 | 531 | | } |
| | | 532 | | |
| | | 533 | | private static void ReturnFlowFieldGuide(FlowFieldGuide guide, bool dispose) |
| | | 534 | | { |
| | 529 | 535 | | if (dispose || guide.GetType() != typeof(FlowFieldGuide)) |
| | 3 | 536 | | _flowFieldGuides.Destroy(guide); |
| | | 537 | | else |
| | 526 | 538 | | _flowFieldGuides.Release(guide); |
| | 526 | 539 | | } |
| | | 540 | | |
| | | 541 | | private static void ReturnVolumeGuide(VolumeGuide guide, bool dispose) |
| | | 542 | | { |
| | 279 | 543 | | if (dispose) |
| | 2 | 544 | | _volumeGuides.Destroy(guide); |
| | | 545 | | else |
| | 277 | 546 | | _volumeGuides.Release(guide); |
| | 277 | 547 | | } |
| | | 548 | | |
| | | 549 | | private static void ClearGuidePools() |
| | | 550 | | { |
| | 40 | 551 | | _aStarGuides.Clear(); |
| | 40 | 552 | | _flowFieldGuides.Clear(); |
| | 40 | 553 | | _volumeGuides.Clear(); |
| | 40 | 554 | | } |
| | | 555 | | |
| | | 556 | | private static AStarSurveyResult ResolveAStarResult(AStarPathRequest request) |
| | | 557 | | { |
| | 72 | 558 | | AStarSurveyResult directResult = _aStarSurveyor.FindPath(request); |
| | 72 | 559 | | if (directResult.HasPath || !request.AllowTraversalTransitions) |
| | 54 | 560 | | return directResult; |
| | | 561 | | |
| | 18 | 562 | | return TryBuildTransitionFallbackAStarResult(request, out AStarSurveyResult fallbackResult) |
| | 18 | 563 | | ? fallbackResult |
| | 18 | 564 | | : directResult; |
| | | 565 | | } |
| | | 566 | | |
| | | 567 | | private static bool TryBuildTransitionFallbackAStarResult( |
| | | 568 | | AStarPathRequest request, |
| | | 569 | | out AStarSurveyResult result) |
| | | 570 | | { |
| | 18 | 571 | | result = AStarSurveyResult.Empty; |
| | | 572 | | |
| | 18 | 573 | | HybridPathRequest? hybridRequest = HybridPathRequest.CreateFromAStar(request); |
| | 18 | 574 | | HybridRoutePlan? routePlan = hybridRequest?.RoutePlan; |
| | 18 | 575 | | if (routePlan == null |
| | 18 | 576 | | || routePlan.DirectedTransitions.Length == 0) |
| | | 577 | | { |
| | 1 | 578 | | return false; |
| | | 579 | | } |
| | | 580 | | |
| | 17 | 581 | | if (!HybridWaypointFlattener.TryBuild( |
| | 17 | 582 | | routePlan, |
| | 17 | 583 | | out AStarWaypoint[]? flattenedWaypoints, |
| | 17 | 584 | | out string[] chartKeys)) |
| | | 585 | | { |
| | 0 | 586 | | return false; |
| | | 587 | | } |
| | | 588 | | |
| | 17 | 589 | | result = AStarSurveyResult.Create(request.Context, flattenedWaypoints!, chartKeys, request.RequestCacheKey); |
| | 17 | 590 | | return true; |
| | | 591 | | } |
| | | 592 | | |
| | | 593 | | private static bool TryBuildTransitionFallbackFlowGuide( |
| | | 594 | | FlowFieldPathRequest request, |
| | | 595 | | [NotNullWhen(true)] out FlowFieldGuide? guide) |
| | | 596 | | { |
| | 7 | 597 | | guide = null; |
| | | 598 | | |
| | 7 | 599 | | if (!TryGetTransitionFallbackFlowPlan(request, out HybridRoutePlan? routePlan)) |
| | 1 | 600 | | return false; |
| | | 601 | | |
| | 6 | 602 | | FlowFieldGuide stagedGuide = _flowFieldGuides.Rent(); |
| | 6 | 603 | | if (stagedGuide.InitializeStaged(routePlan)) |
| | | 604 | | { |
| | 6 | 605 | | guide = stagedGuide; |
| | 6 | 606 | | return true; |
| | | 607 | | } |
| | | 608 | | |
| | 0 | 609 | | ReturnFlowFieldGuide(stagedGuide, dispose: false); |
| | 0 | 610 | | return false; |
| | | 611 | | } |
| | | 612 | | |
| | | 613 | | private static bool TryGetTransitionFallbackFlowPlan( |
| | | 614 | | FlowFieldPathRequest request, |
| | | 615 | | [NotNullWhen(true)] out HybridRoutePlan? routePlan) |
| | | 616 | | { |
| | 7 | 617 | | routePlan = null; |
| | 7 | 618 | | bool pathFound = _cachedHybridRoutePlans.TryGetOrCreate( |
| | 7 | 619 | | request, |
| | 7 | 620 | | () => ResolveTransitionFallbackFlowPlan(request), |
| | 7 | 621 | | out HybridRoutePlanSurveyResult result); |
| | | 622 | | |
| | 7 | 623 | | if (!pathFound || result.RoutePlan == null) |
| | 1 | 624 | | return false; |
| | | 625 | | |
| | 6 | 626 | | routePlan = result.RoutePlan; |
| | 6 | 627 | | _cachedHybridRoutePlans.Return(result, dispose: false); |
| | 6 | 628 | | return true; |
| | | 629 | | } |
| | | 630 | | |
| | | 631 | | private static bool TryGetCachedTransitionFallbackFlowPlan( |
| | | 632 | | FlowFieldPathRequest request, |
| | | 633 | | [NotNullWhen(true)] out HybridRoutePlan? routePlan) |
| | | 634 | | { |
| | 14 | 635 | | routePlan = null; |
| | 14 | 636 | | if (!_cachedHybridRoutePlans.TryCheckout(request, out HybridRoutePlanSurveyResult result)) |
| | 13 | 637 | | return false; |
| | | 638 | | |
| | | 639 | | try |
| | | 640 | | { |
| | 1 | 641 | | routePlan = result.RoutePlan; |
| | 1 | 642 | | return routePlan != null; |
| | | 643 | | } |
| | | 644 | | finally |
| | | 645 | | { |
| | 1 | 646 | | _cachedHybridRoutePlans.Return(result, dispose: false); |
| | 1 | 647 | | } |
| | 1 | 648 | | } |
| | | 649 | | |
| | | 650 | | private static HybridRoutePlanSurveyResult ResolveTransitionFallbackFlowPlan(FlowFieldPathRequest request) |
| | | 651 | | { |
| | 7 | 652 | | HybridPathRequest? hybridRequest = HybridPathRequest.CreateFromFlowField(request); |
| | 7 | 653 | | HybridRoutePlan? routePlan = hybridRequest?.RoutePlan; |
| | 7 | 654 | | if (routePlan == null |
| | 7 | 655 | | || routePlan.DirectedTransitions.Length == 0) |
| | | 656 | | { |
| | 1 | 657 | | return HybridRoutePlanSurveyResult.Empty; |
| | | 658 | | } |
| | | 659 | | |
| | 6 | 660 | | return HybridRoutePlanSurveyResult.Create( |
| | 6 | 661 | | request.Context, |
| | 6 | 662 | | routePlan, |
| | 6 | 663 | | CollectRoutePlanChartKeys(routePlan), |
| | 6 | 664 | | request.RequestCacheKey); |
| | | 665 | | } |
| | | 666 | | |
| | | 667 | | private static string[] CollectRoutePlanChartKeys(HybridRoutePlan routePlan) |
| | | 668 | | { |
| | 9 | 669 | | if (routePlan == null || routePlan.Steps.Length == 0) |
| | 1 | 670 | | return Array.Empty<string>(); |
| | | 671 | | |
| | 8 | 672 | | SwiftHashSet<string> chartKeys = new(); |
| | 68 | 673 | | for (int i = 0; i < routePlan.Steps.Length; i++) |
| | | 674 | | { |
| | 26 | 675 | | HybridRouteStep step = routePlan.Steps[i]; |
| | 26 | 676 | | if (step.Kind != HybridRouteStepKind.PathSegment) |
| | | 677 | | continue; |
| | | 678 | | |
| | 14 | 679 | | if (step.SegmentChartKeys.Length > 0) |
| | 13 | 680 | | AddChartKeys(chartKeys, step.SegmentChartKeys); |
| | | 681 | | else |
| | 1 | 682 | | AddRequestEndpointChartOwners(chartKeys, step.SegmentRequest); |
| | | 683 | | } |
| | | 684 | | |
| | 8 | 685 | | if (chartKeys.Count == 0) |
| | 1 | 686 | | return Array.Empty<string>(); |
| | | 687 | | |
| | 7 | 688 | | string[] result = new string[chartKeys.Count]; |
| | 7 | 689 | | int index = 0; |
| | 54 | 690 | | foreach (string chartKey in chartKeys) |
| | 20 | 691 | | result[index++] = chartKey; |
| | | 692 | | |
| | 7 | 693 | | return result; |
| | | 694 | | } |
| | | 695 | | |
| | | 696 | | private static void AddChartKeys(SwiftHashSet<string> chartKeys, string[] segmentChartKeys) |
| | | 697 | | { |
| | 14 | 698 | | if (chartKeys == null || segmentChartKeys == null) |
| | 1 | 699 | | return; |
| | | 700 | | |
| | 66 | 701 | | for (int i = 0; i < segmentChartKeys.Length; i++) |
| | | 702 | | { |
| | 20 | 703 | | string chartKey = segmentChartKeys[i]; |
| | 20 | 704 | | if (!string.IsNullOrEmpty(chartKey)) |
| | 20 | 705 | | chartKeys.Add(chartKey); |
| | | 706 | | } |
| | 13 | 707 | | } |
| | | 708 | | |
| | | 709 | | private static void AddRequestEndpointChartOwners(SwiftHashSet<string> chartKeys, IPathRequest request) |
| | | 710 | | { |
| | 2 | 711 | | if (request == null) |
| | 1 | 712 | | return; |
| | | 713 | | |
| | 1 | 714 | | AddVoxelChartOwners(chartKeys, request.StartNode); |
| | 1 | 715 | | AddVoxelChartOwners(chartKeys, request.EndNode); |
| | 1 | 716 | | } |
| | | 717 | | |
| | | 718 | | private static void AddVoxelChartOwners(SwiftHashSet<string> chartKeys, Voxel? voxel) |
| | | 719 | | { |
| | 3 | 720 | | if (voxel == null) |
| | 1 | 721 | | return; |
| | | 722 | | |
| | 2 | 723 | | if (voxel.TryGetPartition(out SolidChartPartition? solidPartition) && solidPartition != null) |
| | 1 | 724 | | ChartOwnerUtility.AddOwners(chartKeys, solidPartition.ChartOwners); |
| | | 725 | | |
| | 2 | 726 | | if (voxel.TryGetPartition(out VolumeChartPartition? volumePartition) && volumePartition != null) |
| | 1 | 727 | | ChartOwnerUtility.AddOwners(chartKeys, volumePartition.ChartOwners); |
| | 2 | 728 | | } |
| | | 729 | | } |