< Summary

Information
Class: Trailblazer.Pathing.FlowFieldGuide
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/FlowField/FlowFieldGuide.cs
Line coverage
97%
Covered lines: 168
Uncovered lines: 4
Coverable lines: 172
Total lines: 433
Line coverage: 97.6%
Branch coverage
81%
Covered branches: 112
Total branches: 138
Branch coverage: 81.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/FlowField/FlowFieldGuide.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using GridForge.Spatial;
 4using SwiftCollections;
 5using System;
 6using System.Diagnostics.CodeAnalysis;
 7
 8namespace Trailblazer.Pathing;
 9
 10/// <summary>
 11/// Provides steering direction based on a flow field vector grid.
 12/// Suitable for group-based or gradient-following movement strategies.
 13/// </summary>
 14public class FlowFieldGuide : IGuide
 15{
 16    /// <summary>
 17    /// The maximum distance to search for a valid flow vector when the agent is outside the flow field bounds.
 18    /// </summary>
 019    public static readonly Fixed64 DefaultFieldSearchRange = new(10);
 20
 21    /// <summary>
 22    /// The result of the flow field survey, containing the vector field and path information needed to guide an agent a
 23    /// </summary>
 24    public FlowFieldSurveyResult? FlowMap { get; private set; }
 25
 26    #region Staged Guide State
 27
 28    /// <summary>
 29    /// When executing a hybrid route plan, the guide may need to stage multiple sub-guides for each step of the plan (e
 30    /// </summary>
 31    private HybridRoutePlan? _stagedPlan;
 32
 33    private TrailblazerWorldContext? _stagedContext;
 34
 35    /// <summary>
 36    /// The index of the currently active step within the staged plan.
 37    /// This allows the guide to track progression through the plan and determine which sub-guide to use for providing d
 38    /// </summary>
 39    private int _stagedStepIndex;
 40
 41    /// <summary>
 42    /// The currently active sub-guide for the active step in the staged plan.
 43    /// This is used to delegate direction requests to the appropriate guide based on the current step type (e.g. flow f
 44    /// </summary>
 45    private IGuide? _activeStageGuide;
 46
 47    private TrailblazerWorldContext? _activeStageGuideContext;
 48
 49    /// <summary>
 50    /// The index of the currently active step guide within the staged plan.
 51    /// </summary>
 3552    private int _activeStageGuideStepIndex = -1;
 53
 54    /// <summary>
 55    /// Indicates whether the guide is currently executing a staged plan with an active sub-guide.
 56    /// </summary>
 6357    internal bool IsStaged => _stagedPlan != null;
 58
 52959    internal TrailblazerWorldContext? OwnerContext => FlowMap?.Context ?? _stagedContext ?? _activeStageGuideContext;
 60
 61    #endregion
 62
 63    /// <summary>
 64    /// Initializes the guide with the given flow field survey result.
 65    /// </summary>
 66    /// <param name="surveyResult">The result of the flow field survey containing the vector field and path information.
 67    /// <returns>True if the guide is successfully initialized with a valid path; otherwise, false.</returns>
 68    public bool Initialize(FlowFieldSurveyResult surveyResult)
 69    {
 54070        if (!surveyResult.HasPath)
 171            return false;
 72
 53973        ReleaseStagedResources(dispose: false);
 53974        FlowMap = surveyResult;
 75
 53976        return true;
 77    }
 78
 79    /// <summary>
 80    /// Initializes the guide with a staged hybrid route plan, which may contain multiple steps with different guide typ
 81    /// </summary>
 82    /// <param name="routePlan">The hybrid route plan containing multiple steps with different guide types.</param>
 83    /// <returns>True if the guide is successfully initialized with a valid staged plan; otherwise, false.</returns>
 84    internal bool InitializeStaged(HybridRoutePlan? routePlan)
 85    {
 1586        if (routePlan == null || routePlan.Steps.Length == 0)
 287            return false;
 88
 1389        ReleaseStagedResources(dispose: false);
 1390        FlowMap = null;
 1391        _stagedPlan = routePlan;
 1392        _stagedContext = ResolveStagedContext(routePlan);
 1393        _stagedStepIndex = 0;
 1394        _activeStageGuideStepIndex = -1;
 1395        return true;
 96    }
 97
 98    /// <inheritdoc/>
 99    public bool TryGetMovementDirection(Vector3d origin, out Vector3d direction)
 100    {
 41101        direction = Vector3d.Zero;
 41102        if (IsStaged)
 22103            return TryGetStagedMovementDirection(origin, out direction);
 104
 19105        SwiftDictionary<WorldVoxelIndex, FlowField>? fields = FlowMap?.Fields;
 19106        TrailblazerWorldContext? context = FlowMap?.Context;
 19107        if (FlowMap == null || !FlowMap.HasPath || fields == null || context == null)
 2108            return false;
 109
 17110        direction = FlowFieldSurveyor.SampleFlowVector(context, origin, FlowMap);
 17111        if (direction == Vector3d.Zero)
 2112            return false;
 113
 15114        direction = direction.Normal;
 15115        return true;
 116    }
 117
 118    /// <summary>
 119    /// Determines whether the flow field contains a valid vector for the given position, which can be used to determine
 120    /// </summary>
 121    /// <param name="origin">The position to check within the flow field.</param>
 122    /// <returns>True if the flow field contains a valid vector for the given position; otherwise, false.</returns>
 123    public bool FlowFieldContainsPosition(Vector3d origin)
 124    {
 8125        if (IsStaged)
 126        {
 2127            return _activeStageGuide is FlowFieldGuide stagedFlowGuide
 2128                && stagedFlowGuide.FlowFieldContainsPosition(origin);
 129        }
 130
 6131        SwiftDictionary<WorldVoxelIndex, FlowField>? fields = FlowMap?.Fields;
 6132        TrailblazerWorldContext? context = FlowMap?.Context;
 6133        if (FlowMap == null
 6134            || !FlowMap.HasPath
 6135            || fields == null
 6136            || context == null
 6137            || !context.World.TryGetVoxel(origin, out Voxel? currentVoxel)
 6138            || currentVoxel == null
 6139            || !fields.ContainsKey(currentVoxel.WorldIndex))
 140        {
 3141            return false;
 142        }
 143
 3144        return true;
 145    }
 146
 147    /// <inheritdoc/>
 148    public bool TryGetFallbackDirection(Vector3d origin, out Vector3d fallbackDirection)
 149    {
 8150        fallbackDirection = Vector3d.Zero;
 8151        if (IsStaged)
 4152            return TryGetStagedFallbackDirection(origin, out fallbackDirection);
 153
 4154        SwiftDictionary<WorldVoxelIndex, FlowField>? fields = FlowMap?.Fields;
 4155        TrailblazerWorldContext? context = FlowMap?.Context;
 4156        if (FlowMap == null
 4157            || !FlowMap.HasPath
 4158            || fields == null
 4159            || context == null
 4160            || !context.World.TryGetVoxel(origin, out Voxel? currentVoxel)
 4161            || currentVoxel == null
 4162            || !fields.ContainsKey(currentVoxel.WorldIndex))
 163        {
 2164            return false;
 165        }
 166
 167        // Once the current voxel is already part of the flow map, its center is the nearest valid anchor.
 2168        fallbackDirection = (currentVoxel.WorldPosition - origin).Normalize();
 2169        return true;
 170    }
 171
 172    /// <summary>
 173    /// Releases any resources associated with the currently staged plan and active stage guide, optionally disposing of
 174    /// </summary>
 175    /// <param name="dispose">Indicates whether to dispose of the active guide if it implements IDisposable.</param>
 176    internal void ReleaseStagedResources(bool dispose)
 177    {
 1086178        if (_activeStageGuide != null)
 179        {
 2180            TrailblazerWorldContext context = _activeStageGuideContext
 2181                ?? throw new InvalidOperationException("Active stage guide is missing its TrailblazerWorldContext.");
 2182            context.Guides.ReturnGuide(_activeStageGuide, dispose);
 2183            _activeStageGuide = null;
 184        }
 185
 1086186        _activeStageGuideContext = null;
 1086187        _activeStageGuideStepIndex = -1;
 1086188        _stagedPlan = null;
 1086189        _stagedContext = null;
 1086190        _stagedStepIndex = 0;
 1086191    }
 192
 193    /// <summary>
 194    /// Attempts to get the movement direction from the currently active stage guide within the staged plan.
 195    /// </summary>
 196    /// <param name="origin">The position from which to determine the movement direction.</param>
 197    /// <param name="direction">The resulting movement direction, if available.</param>
 198    /// <returns>True if a valid movement direction was obtained; otherwise, false.</returns>
 199    private bool TryGetStagedMovementDirection(Vector3d origin, out Vector3d direction)
 200    {
 22201        direction = Vector3d.Zero;
 22202        HybridRoutePlan stagedPlan = _stagedPlan!;
 22203        int remainingStageAdvances = stagedPlan.Steps.Length;
 22204        while (TryGetPreparedStage(origin, ref remainingStageAdvances, out HybridRouteStep? currentStep))
 205        {
 19206            switch (currentStep.Kind)
 207            {
 208                case HybridRouteStepKind.Waypoint:
 209                    // TryGetPreparedStage(...) already skipped completed waypoint stages, so a yielded waypoint
 210                    // must still be ahead of the caller and therefore resolves to a non-zero direction.
 8211                    direction = (currentStep.WaypointPosition - origin).Normalize();
 8212                    return true;
 213
 214                case HybridRouteStepKind.PathSegment:
 11215                    if (!TryGetSegmentStageMovementDirection(
 11216                        origin,
 11217                        currentStep,
 11218                        ref remainingStageAdvances,
 11219                        out direction))
 220                    {
 2221                        return false;
 222                    }
 223
 9224                    if (direction != Vector3d.Zero)
 9225                        return true;
 226
 227                    break;
 228            }
 229        }
 230
 3231        return false;
 232    }
 233
 234    private bool TryGetSegmentStageMovementDirection(
 235        Vector3d origin,
 236        HybridRouteStep currentStep,
 237        ref int remainingStageAdvances,
 238        out Vector3d direction)
 239    {
 12240        direction = Vector3d.Zero;
 12241        if (!TryGetOrCreateActiveStageGuide(currentStep, out IGuide? activeGuide))
 1242            return false;
 243
 11244        if (activeGuide is IWaypointGuide waypointGuide)
 2245            direction = waypointGuide.GetCurrentWaypointDirection(origin);
 246        else
 9247            activeGuide.TryGetMovementDirection(origin, out direction);
 248
 11249        return direction != Vector3d.Zero
 11250            || (IsStageTargetReached(origin, currentStep)
 11251                && TryAdvanceStage(ref remainingStageAdvances));
 252    }
 253
 254    /// <summary>
 255    /// Attempts to get a fallback movement direction from the currently active stage guide within the staged plan, star
 256    /// This allows the guide to provide fallback directions from upcoming stages in the plan if the current stage does 
 257    /// </summary>
 258    /// <param name="origin">The position from which to determine the fallback movement direction.</param>
 259    /// <param name="fallbackDirection">The resulting fallback movement direction, if available.</param>
 260    /// <returns>True if a valid fallback movement direction was obtained; otherwise, false.</returns>
 261    private bool TryGetStagedFallbackDirection(Vector3d origin, out Vector3d fallbackDirection)
 262    {
 4263        fallbackDirection = Vector3d.Zero;
 4264        HybridRoutePlan stagedPlan = _stagedPlan!;
 4265        int remainingStageAdvances = stagedPlan.Steps.Length;
 4266        if (!TryGetPreparedStage(origin, ref remainingStageAdvances, out HybridRouteStep? currentStep))
 1267            return false;
 268
 3269        if (currentStep.Kind == HybridRouteStepKind.Waypoint)
 270        {
 1271            fallbackDirection = (currentStep.WaypointPosition - origin).Normalize();
 1272            return true;
 273        }
 274
 2275        return TryGetOrCreateActiveStageGuide(currentStep, out IGuide? activeGuide)
 2276            && activeGuide != null
 2277            && activeGuide.TryGetFallbackDirection(origin, out fallbackDirection);
 278    }
 279
 280    /// <summary>
 281    /// Advances through already-completed stages with a bounded budget so malformed staged plans cannot
 282    /// recurse or spin indefinitely while trying to find the next actionable step.
 283    /// </summary>
 284    private bool TryGetPreparedStage(
 285        Vector3d origin,
 286        ref int remainingStageAdvances,
 287        [NotNullWhen(true)] out HybridRouteStep? currentStep)
 288    {
 103289        while (TryGetCurrentStage(out currentStep))
 290        {
 99291            if (!IsStageTargetReached(origin, currentStep))
 22292                return true;
 293
 77294            if (!TryAdvanceStage(ref remainingStageAdvances))
 295                break;
 296        }
 297
 4298        currentStep = null;
 4299        return false;
 300    }
 301
 302    /// <summary>
 303    /// Attempts to get the currently active stage from the staged plan based on the current stage index, and returns tr
 304    /// </summary>
 305    /// <param name="currentStep">The currently active stage, if found.</param>
 306    /// <returns>True if a valid stage is found; otherwise, false.</returns>
 307    private bool TryGetCurrentStage([NotNullWhen(true)] out HybridRouteStep? currentStep)
 308    {
 103309        currentStep = null;
 103310        if (_stagedPlan == null
 103311            || _stagedStepIndex < 0
 103312            || _stagedStepIndex >= _stagedPlan.Steps.Length)
 313        {
 4314            return false;
 315        }
 316
 99317        currentStep = _stagedPlan.Steps[_stagedStepIndex];
 99318        return currentStep != null;
 319    }
 320
 321    private bool TryAdvanceStage(ref int remainingStageAdvances)
 322    {
 78323        if (remainingStageAdvances <= 0)
 0324            return false;
 325
 78326        remainingStageAdvances--;
 78327        AdvanceStage(dispose: false);
 78328        return true;
 329    }
 330
 331    /// <summary>
 332    /// Advances to the next stage in the staged plan, optionally disposing of the active stage guide if it implements I
 333    /// </summary>
 334    /// <param name="dispose">Indicates whether to dispose of the active stage guide when advancing.</param>
 335    private void AdvanceStage(bool dispose)
 336    {
 78337        ReleaseActiveStageGuide(dispose);
 78338        _stagedStepIndex++;
 78339    }
 340
 341    /// <summary>
 342    /// Releases the currently active stage guide, optionally disposing of it if it implements IDisposable. This is call
 343    /// </summary>
 344    /// <param name="dispose">Indicates whether to dispose of the active stage guide when releasing.</param>
 345    private void ReleaseActiveStageGuide(bool dispose)
 346    {
 91347        if (_activeStageGuide == null)
 85348            return;
 349
 6350        TrailblazerWorldContext context = _activeStageGuideContext
 6351            ?? throw new InvalidOperationException("Active stage guide is missing its TrailblazerWorldContext.");
 6352        context.Guides.ReturnGuide(_activeStageGuide, dispose);
 6353        _activeStageGuide = null;
 6354        _activeStageGuideContext = null;
 6355        _activeStageGuideStepIndex = -1;
 6356    }
 357
 358
 359    /// <summary>
 360    /// Attempts to get or create the active stage guide for the current stage in the staged plan, if the current stage 
 361    /// </summary>
 362    /// <param name="currentStep">The current step in the staged plan.</param>
 363    /// <param name="guide">The active stage guide for the current step, if available.</param>
 364    /// <returns>True if an active stage guide is available or successfully created; otherwise, false.</returns>
 365    private bool TryGetOrCreateActiveStageGuide(HybridRouteStep currentStep, [NotNullWhen(true)] out IGuide? guide)
 366    {
 16367        guide = null;
 16368        if (currentStep.Kind != HybridRouteStepKind.PathSegment)
 0369            return false;
 370
 16371        if (_activeStageGuide != null && _activeStageGuideStepIndex == _stagedStepIndex)
 372        {
 3373            guide = _activeStageGuide;
 3374            return true;
 375        }
 376
 13377        ReleaseActiveStageGuide(dispose: false);
 13378        TrailblazerWorldContext context = currentStep.SegmentRequest.Context;
 13379        if (!context.Guides.RequestGuide(currentStep.SegmentRequest, out _activeStageGuide)
 13380            || _activeStageGuide == null)
 381        {
 2382            return false;
 383        }
 384
 11385        _activeStageGuideContext = context;
 11386        _activeStageGuideStepIndex = _stagedStepIndex;
 11387        guide = _activeStageGuide;
 11388        return true;
 389    }
 390
 391    /// <summary>
 392    /// Determines whether the target for the current stage has been reached based on the agent's position. This is used
 393    /// </summary>
 394    /// <param name="origin">The current position of the agent.</param>
 395    /// <param name="currentStep">The current step in the staged plan.</param>
 396    /// <returns>True if the target for the current stage has been reached; otherwise, false.</returns>
 397    private bool IsStageTargetReached(Vector3d origin, HybridRouteStep currentStep)
 398    {
 399        // HybridRouteStep.Kind is factory-assigned to Waypoint or PathSegment only.
 101400        Vector3d target = currentStep.Kind == HybridRouteStepKind.Waypoint
 101401            ? currentStep.WaypointPosition
 101402            : currentStep.SegmentRequest.TargetPosition;
 403
 101404        TrailblazerWorldContext context = currentStep.Context
 101405            ?? _stagedContext
 101406            ?? throw new InvalidOperationException("Staged flow guide is missing its TrailblazerWorldContext.");
 101407        Fixed64 completionDistance = context.VoxelSize * Fixed64.Half;
 101408        return (target - origin).SqrMagnitude <= completionDistance * completionDistance;
 409    }
 410
 411    private static TrailblazerWorldContext ResolveStagedContext(HybridRoutePlan routePlan)
 412    {
 26413        for (int i = 0; i < routePlan.Steps.Length; i++)
 414        {
 13415            HybridRouteStep? step = routePlan.Steps[i];
 13416            if (step != null)
 13417                return step.Context;
 418        }
 419
 0420        throw new InvalidOperationException("Staged flow guide requires at least one context-bound path segment.");
 421    }
 422
 423    internal void ResetForReuse()
 424    {
 529425        FlowMap = null;
 529426        _stagedPlan = null;
 529427        _stagedContext = null;
 529428        _stagedStepIndex = 0;
 529429        _activeStageGuide = null;
 529430        _activeStageGuideContext = null;
 529431        _activeStageGuideStepIndex = -1;
 529432    }
 433}