< Summary

Information
Class: Trailblazer.Pathing.FlowFieldSurveyor
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/FlowField/FlowFieldSurveyor.cs
Line coverage
96%
Covered lines: 265
Uncovered lines: 9
Coverable lines: 274
Total lines: 648
Line coverage: 96.7%
Branch coverage
91%
Covered branches: 157
Total branches: 172
Branch coverage: 91.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_Shared()100%11100%
.ctor()100%11100%
FindPath(...)92.85%1414100%
FloodPath()100%1212100%
AnalyzeNeighborDistance(...)100%11100%
TryProcessDirection(...)95%202093.75%
ExceedsMaxClimbHeight(...)100%11100%
HasValidDiagonalLegs(...)100%1212100%
IsLegClear(...)83.33%6683.33%
GenerateFlowFields()95.45%222292.3%
AddFlowField(...)100%11100%
GetPathCost(...)100%22100%
GetPathCostTotal(...)100%22100%
ClearWorkingState()100%11100%
PrepareSamplingGrids()100%44100%
GetOrAddSamplingGridBuilder(...)75%44100%
GetSamplingGrid(...)50%4475%
CreateNormalizedDirectionLookup()100%22100%
SampleFlowVector(...)83.33%66100%
SampleFlowVector(...)75%8896%
TryGetNearestFlowAnchor(...)100%1212100%
GetFlowDirection(...)100%66100%
GetFlowDirection(...)87.5%88100%
GetFlowField(...)100%66100%
ThrowIfResultOwnedByDifferentContext(...)50%6450%
.ctor(...)100%11100%
MatchesGrid(...)50%44100%
Include(...)100%1414100%
Create(...)100%11100%

File(s)

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

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using GridForge.Spatial;
 4using SwiftCollections;
 5using System;
 6using System.Linq;
 7using System.Runtime.CompilerServices;
 8using System.Threading;
 9
 10namespace Trailblazer.Pathing;
 11
 12/// <summary>
 13/// Responsible for generating flow fields using a wavefront flood-fill algorithm.
 14/// Provides pathfinding data suitable for many agents with shared destinations.
 15/// </summary>
 16public class FlowFieldSurveyor
 17{
 118    private static readonly Vector3d[] _normalizedDirectionByNeighbor = CreateNormalizedDirectionLookup();
 19
 20    #region Singleton Instances
 21
 22    /// <summary>
 23    /// A lazily initialized singleton instance of the pathfinder.
 24    /// </summary>
 125    private static readonly Lazy<FlowFieldSurveyor> _instance =
 126        new(() => new FlowFieldSurveyor(), LazyThreadSafetyMode.ExecutionAndPublication);
 27
 28    /// <summary>
 29    /// Gets the shared instance of the pathfinder.
 30    /// </summary>
 5031    public static FlowFieldSurveyor Shared => _instance.Value;
 32
 33    #endregion
 34
 96835    private readonly SurveyorLock _scratchLock = new();
 36
 96837    private readonly PathHeap<SolidChartPartition> _heap = new();
 38
 96839    private readonly SwiftHashSet<string> _chartKeys = new();
 40
 96841    private readonly SwiftList<FlowFieldSamplingGrid> _samplingGrids = new();
 42
 96843    private readonly SwiftList<FlowFieldSamplingGridBuilder> _samplingGridBuilders = new();
 44
 45    private FlowFieldPathRequest? _request;
 46
 47    /// <summary>
 48    /// Attempts to create a shared flow field path from the start to the end voxel specified in the request.
 49    /// </summary>
 50    /// <param name="request">A flow field path request containing the start, end, and search parameters.</param>
 51    /// <returns>A dictionary of flow fields indexed by spawn token.</returns>
 52    public FlowFieldSurveyResult FindPath(FlowFieldPathRequest request)
 53    {
 25854        lock (_scratchLock)
 55        {
 25856            if (request == null
 25857            || request.HasZeroDisplacement
 25858            || request.EndNode == null
 25859            || !request.EndNode.TryGetPartition(out SolidChartPartition? targetPart)
 25860            || targetPart == null)
 61            {
 462                return FlowFieldSurveyResult.Empty;
 63            }
 64
 25465            _request = request;
 66
 25467            ClearWorkingState();
 68            // Start from the end and move towards the start voxel
 25469            _heap.Add(targetPart, pathCost: 0);
 25470            ChartOwnerUtility.AddOwners(_chartKeys, targetPart.ChartOwners);
 71
 25472            if (!FloodPath())
 73            {
 13274                ClearWorkingState();
 13275                return FlowFieldSurveyResult.Empty;
 76            }
 77
 12278            SwiftDictionary<WorldVoxelIndex, FlowField> flowFields = GenerateFlowFields();
 12279            string[] chartsUsed = _chartKeys.ToArray();
 12280            FlowFieldSamplingGrid[] samplingGrids = _samplingGrids.Count == 0
 12281                ? Array.Empty<FlowFieldSamplingGrid>()
 12282                : _samplingGrids.ToArray();
 12283            FlowFieldSurveyResult result = FlowFieldSurveyResult.Create(
 12284                request.Context,
 12285                flowFields,
 12286                chartsUsed,
 12287                request.RequestCacheKey,
 12288                samplingGrids);
 12289            ClearWorkingState();
 12290            return result;
 91        }
 25892    }
 93
 94    /// <summary>
 95    /// Executes the wavefront expansion (flood fill) phase of the flow field generation algorithm.
 96    /// Starts from the goal and expands outward until the start voxel is reached or search range is exceeded.
 97    /// </summary>
 98    /// <returns><c>true</c> if the start voxel is reached within the maximum range; otherwise <c>false</c>.</returns>
 99    private bool FloodPath()
 100    {
 254101        FlowFieldPathRequest request = _request!;
 254102        WorldVoxelIndex startIndex = request.StartNode!.WorldIndex;
 254103        bool targetReached = false;
 104
 254105        int iterations = 0;
 254106        int searchSize = request.MaxPathSearchRange;
 254107        int maxFloodRange = 0;
 108
 2385109        while (_heap.RemoveFirst(out SolidChartPartition? current)
 2385110            && current != null
 2385111            && iterations++ < searchSize)
 112        {
 2132113            int currentPathCost = GetPathCost(current);
 114
 115            // Check if we found our way to the start voxel
 2132116            if (!targetReached)
 117            {
 1934118                if (current.WorldIndex == startIndex)
 119                {
 122120                    maxFloodRange = currentPathCost + request.ExtraFloodRange;
 122121                    targetReached = true;
 122                }
 123
 124            }
 198125            else if (currentPathCost >= maxFloodRange)
 126                break;
 127
 2131128            AnalyzeNeighborDistance(current, currentPathCost);
 129
 2131130            _heap.SetClosed(current);
 2131131        }
 132
 254133        return targetReached;
 134    }
 135
 136    /// <summary>
 137    /// Evaluates each walkable neighbor of the current partition and assigns a heap cost if a shorter path is found.
 138    /// Ensures the wavefront expands in an optimal order.
 139    /// </summary>
 140    /// <param name="current">The current path partition being evaluated.</param>
 141    /// <param name="currentPathCost">The current flood distance stored for the partition.</param>
 142    private void AnalyzeNeighborDistance(SolidChartPartition current, int currentPathCost)
 143    {
 2131144        TryProcessDirection(current, SpatialAwareness.PerpendicularDirections, currentPathCost);
 2131145        TryProcessDirection(current, SpatialAwareness.DiagonalDirections, currentPathCost, true);
 2131146    }
 147
 148    private void TryProcessDirection(
 149        SolidChartPartition current,
 150        SpatialDirection[] directions,
 151        int currentPathCost,
 152        bool checkEdges = false)
 153    {
 4263154        FlowFieldPathRequest request = _request!;
 4263155        SolidChartPartition?[]? neighbors = current.Neighbors;
 4263156        if (neighbors == null)
 0157            return;
 158
 119340159        foreach (SpatialDirection dir in directions)
 160        {
 55407161            SolidChartPartition? neighbor = neighbors[(int)dir];
 55407162            if (neighbor is null || _heap.IsClosed(neighbor) || neighbor.IsImpassable(request.UnitSize))
 163                continue;
 164
 6524165            if (ExceedsMaxClimbHeight(current, neighbor))
 166                continue;
 167
 5560168            if (checkEdges && !HasValidDiagonalLegs(current, dir))
 169                continue;
 170
 3044171            int newCost = currentPathCost + 1;
 172
 3044173            if (!_heap.Contains(neighbor))
 174            {
 1879175                _heap.Add(neighbor, newCost);
 176            }
 1165177            else if (GetPathCost(neighbor) > newCost)
 178            {
 1179                _heap.UpdatePathCost(neighbor, newCost);
 1180                _heap.SortUp(neighbor);
 181            }
 182        }
 4263183    }
 184
 185    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 186    private bool ExceedsMaxClimbHeight(SolidChartPartition current, SolidChartPartition neighbor)
 187    {
 16783188        FlowFieldPathRequest request = _request!;
 16783189        Fixed64 heightDifference = (current.VoxelPosition.y - neighbor.VoxelPosition.y).Abs();
 16783190        return heightDifference > request.MaxClimbHeight;
 191    }
 192
 193    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 194    private bool HasValidDiagonalLegs(SolidChartPartition current, SpatialDirection diagonal)
 195    {
 7240196        (int dx, int dy, int dz) = SpatialAwareness.DirectionOffsets[(int)diagonal];
 197
 7240198        if (dx != 0 && !IsLegClear(current, DiagonalTraversalLegs.ForXOffset(dx)))
 1949199            return false;
 200
 5291201        if (dy != 0 && !IsLegClear(current, DiagonalTraversalLegs.ForYOffset(dy)))
 2202            return false;
 203
 5289204        if (dz != 0 && !IsLegClear(current, DiagonalTraversalLegs.ForZOffset(dz)))
 685205            return false;
 206
 4604207        return true;
 208    }
 209
 210    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 211    private bool IsLegClear(SolidChartPartition current, SpatialDirection legDir)
 212    {
 12531213        FlowFieldPathRequest request = _request!;
 12531214        SolidChartPartition?[]? neighbors = current.Neighbors;
 12531215        if (neighbors == null)
 0216            return false;
 217
 12531218        SolidChartPartition? leg = neighbors[(int)legDir];
 12531219        return leg != null && _heap.IsClosed(leg) && !leg.IsImpassable(request.UnitSize);
 220    }
 221
 222    /// <summary>
 223    /// Converts the results of the flood fill phase into directional flow fields pointing toward the goal.
 224    /// Each partition is assigned a direction vector blending shortest path and direct-to-goal direction.
 225    /// </summary>
 226    /// <returns>A dictionary of directional flow field data indexed by voxel spawn tokens.</returns>
 227    private SwiftDictionary<WorldVoxelIndex, FlowField> GenerateFlowFields()
 228    {
 122229        FlowFieldPathRequest request = _request!;
 122230        WorldVoxelIndex endIndex = request.EndNode!.WorldIndex;
 122231        PrepareSamplingGrids();
 232
 122233        SwiftDictionary<WorldVoxelIndex, FlowField> result = new(_heap.TrackedCount);
 234        // Fixed64 totalDistance = Fixed64.One + _startDistanceMetric; // + 1 for end part
 235
 3876236        foreach (SolidChartPartition current in _heap.EnumerateClosed())
 237        {
 1816238            FlowFieldSamplingGrid samplingGrid = GetSamplingGrid(current.WorldIndex);
 239
 1816240            FlowField currentFlow = new()
 1816241            {
 1816242                GlobalIndex = current.WorldIndex,
 1816243                PathCost = GetPathCostTotal(current)
 1816244            };
 245
 1816246            if (current.WorldIndex == endIndex)
 247            {
 248                // Ensure end voxel is include, it shouldn't point anywhere
 122249                currentFlow.IsGoal = true;
 122250                AddFlowField(result, samplingGrid, current.WorldIndex, currentFlow);
 122251                continue;
 252            }
 253
 254            // Go through all neighbours and find the one with the lowest distance
 1694255            SolidChartPartition? minPartition = null;
 1694256            int minCost = int.MaxValue;
 1694257            int minDirectionIndex = -1;
 1694258            SolidChartPartition?[]? neighbors = current.Neighbors;
 1694259            if (neighbors == null)
 260            {
 0261                AddFlowField(result, samplingGrid, current.WorldIndex, currentFlow);
 0262                ChartOwnerUtility.AddOwners(_chartKeys, current.ChartOwners);
 0263                continue;
 264            }
 265
 91476266            for (int i = 0; i < neighbors.Length; i++)
 267            {
 44044268                SolidChartPartition? nPart = neighbors[i];
 269                // check closed heap version to ensure neighbor was part of flood phase
 44044270                if (nPart == null || !_heap.IsClosed(nPart))
 271                    continue;
 272
 10259273                if (ExceedsMaxClimbHeight(current, nPart))
 274                    continue;
 275
 10259276                if (SpatialAwareness.IsDiagonalNeighbor((SpatialDirection)i)
 10259277                    && !HasValidDiagonalLegs(current, (SpatialDirection)i))
 278                {
 279                    continue;
 280                }
 281
 10140282                int cost = GetPathCostTotal(nPart);
 10140283                if (cost < minCost)
 284                {
 4214285                    minPartition = nPart;
 4214286                    minCost = cost;
 4214287                    minDirectionIndex = i;
 288                }
 289            }
 290
 291            // If we found a valid neighbour, point in its direction by applying distance-weighted blending
 1694292            if (minPartition != null)
 1694293                currentFlow.Direction = _normalizedDirectionByNeighbor[minDirectionIndex];
 294
 1694295            AddFlowField(result, samplingGrid, current.WorldIndex, currentFlow);
 1694296            ChartOwnerUtility.AddOwners(_chartKeys, current.ChartOwners);
 297        }
 298
 122299        return result;
 300    }
 301
 302    private static void AddFlowField(
 303        SwiftDictionary<WorldVoxelIndex, FlowField> fields,
 304        FlowFieldSamplingGrid samplingGrid,
 305        WorldVoxelIndex index,
 306        FlowField field)
 307    {
 1816308        fields.Add(index, field);
 1816309        samplingGrid.AddDirection(index, field.Direction);
 1816310    }
 311
 312    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 313    private int GetPathCost(SolidChartPartition partition)
 314    {
 15254315        return _heap.TryGetPathCost(partition, out int pathCost)
 15254316            ? pathCost
 15254317            : int.MaxValue;
 318    }
 319
 320    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 321    private int GetPathCostTotal(SolidChartPartition partition)
 322    {
 11957323        int pathCost = GetPathCost(partition);
 11957324        return pathCost == int.MaxValue
 11957325            ? int.MaxValue
 11957326            : pathCost + partition.PathCostModifier;
 327    }
 328
 329    private void ClearWorkingState()
 330    {
 508331        _heap.FastClear();
 508332        _chartKeys.Clear();
 508333        _samplingGrids.Clear();
 508334        _samplingGridBuilders.Clear();
 508335    }
 336
 337    private void PrepareSamplingGrids()
 338    {
 122339        _samplingGridBuilders.Clear();
 122340        _samplingGrids.Clear();
 341
 3876342        foreach (SolidChartPartition current in _heap.EnumerateClosed())
 343        {
 1816344            FlowFieldSamplingGridBuilder builder = GetOrAddSamplingGridBuilder(current.WorldIndex, current.VoxelPosition
 1816345            builder.Include(current.WorldIndex);
 346        }
 347
 488348        for (int i = 0; i < _samplingGridBuilders.Count; i++)
 122349            _samplingGrids.Add(_samplingGridBuilders[i].Create(_request!.Context.VoxelSize));
 122350    }
 351
 352    private FlowFieldSamplingGridBuilder GetOrAddSamplingGridBuilder(WorldVoxelIndex index, Vector3d worldPosition)
 353    {
 3632354        for (int i = 0; i < _samplingGridBuilders.Count; i++)
 355        {
 1694356            if (_samplingGridBuilders[i].MatchesGrid(index))
 1694357                return _samplingGridBuilders[i];
 358        }
 359
 122360        FlowFieldSamplingGridBuilder builder = new(index, worldPosition, _request!.Context.VoxelSize);
 122361        _samplingGridBuilders.Add(builder);
 122362        return builder;
 363    }
 364
 365    private FlowFieldSamplingGrid GetSamplingGrid(WorldVoxelIndex index)
 366    {
 3632367        for (int i = 0; i < _samplingGrids.Count; i++)
 368        {
 1816369            if (_samplingGrids[i].MatchesGrid(index))
 1816370                return _samplingGrids[i];
 371        }
 372
 0373        throw new InvalidOperationException("Flow-field sampling grid was not prepared for the closed partition.");
 374    }
 375
 376    private static Vector3d[] CreateNormalizedDirectionLookup()
 377    {
 1378        Vector3d[] directions = new Vector3d[SpatialAwareness.DirectionOffsets.Length];
 54379        for (int i = 0; i < directions.Length; i++)
 380        {
 26381            (int x, int y, int z) = SpatialAwareness.DirectionOffsets[i];
 26382            directions[i] = new Vector3d((Fixed64)x, (Fixed64)y, (Fixed64)z).Normalize();
 383        }
 384
 1385        return directions;
 386    }
 387
 388    /// <summary>
 389    /// Samples an interpolated flow direction from a survey result using direct index arithmetic.
 390    /// </summary>
 391    /// <param name="context">The world context to sample against.</param>
 392    /// <param name="worldPosition">The world-space position to sample from.</param>
 393    /// <param name="result">The flow-field survey result to sample.</param>
 394    /// <returns>An interpolated directional vector.</returns>
 395    public static Vector3d SampleFlowVector(
 396        TrailblazerWorldContext context,
 397        Vector3d worldPosition,
 398        FlowFieldSurveyResult result)
 399    {
 39400        if (result == null || !result.HasPath || result.Fields == null)
 1401            return Vector3d.Zero;
 402
 38403        PathRequestContextResolver.ThrowIfUnusable(context);
 38404        ThrowIfResultOwnedByDifferentContext(result, context);
 405
 38406        return SampleFlowVector(context, worldPosition, result.Fields, result.SamplingGrids);
 407    }
 408
 409    private static Vector3d SampleFlowVector(
 410        TrailblazerWorldContext context,
 411        Vector3d worldPosition,
 412        SwiftDictionary<WorldVoxelIndex, FlowField> fields,
 413        FlowFieldSamplingGrid[]? samplingGrids)
 414    {
 38415        if (fields == null || fields.Count == 0)
 0416            return Vector3d.Zero;
 417
 38418        Fixed64 voxelSize = context.VoxelSize;
 419
 420        // Get bottom-left corner of the square the agent is standing in
 38421        Vector3d corner = new(
 38422            FixedMath.Floor(worldPosition.x / voxelSize) * voxelSize,
 38423            FixedMath.Floor(worldPosition.y / voxelSize) * voxelSize,
 38424            FixedMath.Floor(worldPosition.z / voxelSize) * voxelSize
 38425        );
 426
 427        // Compute normalized offset in cell (0..1)
 38428        Fixed64 dx = (worldPosition.x - corner.x) / voxelSize;
 38429        Fixed64 dz = (worldPosition.z - corner.z) / voxelSize;
 430
 38431        if (dx == Fixed64.Zero && dz == Fixed64.Zero)
 36432            return GetFlowDirection(context, corner, fields, samplingGrids);
 433
 434        // Sample the 4 surrounding voxel centers
 2435        Vector3d bottomLeft = corner;
 2436        Vector3d bottomRight = corner + new Vector3d(voxelSize, Fixed64.Zero, Fixed64.Zero);
 2437        Vector3d topLeft = corner + new Vector3d(Fixed64.Zero, Fixed64.Zero, voxelSize);
 2438        Vector3d topRight = corner + new Vector3d(voxelSize, Fixed64.Zero, voxelSize);
 439
 440        // Get flow vectors
 2441        Vector3d f00 = GetFlowDirection(context, bottomLeft, fields, samplingGrids);
 2442        Vector3d f10 = GetFlowDirection(context, bottomRight, fields, samplingGrids);
 2443        Vector3d f01 = GetFlowDirection(context, topLeft, fields, samplingGrids);
 2444        Vector3d f11 = GetFlowDirection(context, topRight, fields, samplingGrids);
 445
 446        // Bilinear interpolation
 2447        Vector3d zHigh = f00 * (Fixed64.One - dx) + f10 * dx;
 2448        Vector3d zLow = f01 * (Fixed64.One - dx) + f11 * dx;
 2449        Vector3d blended = zHigh * (Fixed64.One - dz) + zLow * dz;
 450
 2451        blended.Normalize();
 2452        return blended;
 453    }
 454
 455    /// <summary>
 456    /// Attempts to locate the closest valid voxel from which to begin flow-based movement.
 457    /// Useful for finding an initial entry point to the flow field.
 458    /// </summary>
 459    /// <param name="context">The world context to search against.</param>
 460    /// <param name="origin">The world-space origin to search from.</param>
 461    /// <param name="fields">Flow field data indexed by voxel spawn token.</param>
 462    /// <param name="result">The closest valid voxel, if found.</param>
 463    /// <param name="range">Maximum range to search.</param>
 464    /// <returns><c>true</c> if a nearby flow field anchor is found; otherwise <c>false</c>.</returns>
 465    public static bool TryGetNearestFlowAnchor(
 466        TrailblazerWorldContext context,
 467        Vector3d origin,
 468        SwiftDictionary<WorldVoxelIndex, FlowField> fields,
 469        Fixed64 range,
 470        out Voxel? result)
 471    {
 3472        result = null;
 3473        PathRequestContextResolver.ThrowIfUnusable(context);
 3474        if (fields == null || fields.Count == 0)
 1475            return false;
 476
 2477        Fixed64 minDistanceSq = range * range;
 2478        bool found = false;
 479
 12480        foreach (FlowField flow in fields.Values)
 481        {
 4482            if (!context.World.TryGetGridAndVoxel(flow.GlobalIndex, out _, out Voxel? flowVoxel)
 4483                || flowVoxel == null)
 484                continue;
 485
 2486            Fixed64 distSq = Vector3d.SqrDistance(origin, flowVoxel.WorldPosition);
 2487            if (distSq <= minDistanceSq)
 488            {
 1489                result = flowVoxel;
 1490                minDistanceSq = distSq;
 1491                found = true;
 492            }
 493        }
 494
 2495        return found;
 496    }
 497
 498    /// <summary>
 499    /// Retrieves the raw directional flow vector at the given world-space position, if available.
 500    /// </summary>
 501    /// <param name="context">The world context to query against.</param>
 502    /// <param name="position">The position to query within the flow field.</param>
 503    /// <param name="fields">Flow field data indexed by voxel index.</param>
 504    /// <returns>The direction vector, or <c>Vector3d.Zero</c> if no field exists.</returns>
 505    public static Vector3d GetFlowDirection(
 506        TrailblazerWorldContext context,
 507        Vector3d position,
 508        SwiftDictionary<WorldVoxelIndex, FlowField> fields)
 509    {
 3510        PathRequestContextResolver.ThrowIfUnusable(context);
 3511        if (context.World.TryGetVoxel(position, out Voxel? voxel)
 3512            && voxel != null)
 513        {
 3514            if (fields.TryGetValue(voxel.WorldIndex, out FlowField field))
 2515                return field.Direction;
 516        }
 1517        return Vector3d.Zero;
 518    }
 519
 520    private static Vector3d GetFlowDirection(
 521        TrailblazerWorldContext context,
 522        Vector3d position,
 523        SwiftDictionary<WorldVoxelIndex, FlowField> fields,
 524        FlowFieldSamplingGrid[]? samplingGrids)
 525    {
 44526        if (samplingGrids == null || samplingGrids.Length == 0)
 2527            return GetFlowDirection(context, position, fields);
 528
 90529        for (int i = 0; i < samplingGrids.Length; i++)
 530        {
 42531            if (samplingGrids[i].TryGetDirection(position, out Vector3d direction))
 39532                return direction;
 533        }
 534
 3535        return Vector3d.Zero;
 536    }
 537
 538    /// <summary>
 539    /// Retrieves the flow field associated with the specified world position, if available.
 540    /// </summary>
 541    /// <remarks>
 542    /// If the position does not correspond to a valid voxel or no flow field is found for the voxel,
 543    /// the method returns the default value for FlowField.
 544    /// </remarks>
 545    /// <param name="context">The world context to query against.</param>
 546    /// <param name="position">The world position for which to retrieve the corresponding flow field.</param>
 547    /// <param name="fields">A dictionary mapping world voxel indices to their associated flow fields. Must not be null.
 548    /// <returns>
 549    /// The flow field associated with the specified position if found; otherwise, the default value for the FlowField t
 550    /// </returns>
 551    public static FlowField GetFlowField(
 552        TrailblazerWorldContext context,
 553        Vector3d position,
 554        SwiftDictionary<WorldVoxelIndex, FlowField> fields)
 555    {
 4556        PathRequestContextResolver.ThrowIfUnusable(context);
 4557        if (context.World.TryGetVoxel(position, out Voxel? voxel)
 4558            && voxel != null)
 559        {
 4560            if (fields.TryGetValue(voxel.WorldIndex, out FlowField field))
 3561                return field;
 562        }
 1563        return default;
 564    }
 565
 566    private static void ThrowIfResultOwnedByDifferentContext(
 567        FlowFieldSurveyResult result,
 568        TrailblazerWorldContext expectedContext)
 569    {
 38570        if (result.Context == null || ReferenceEquals(result.Context, expectedContext))
 38571            return;
 572
 0573        throw new InvalidOperationException(
 0574            "Flow field survey result belongs to a different owning TrailblazerWorldContext.");
 575    }
 576
 577    private sealed class FlowFieldSamplingGridBuilder
 578    {
 579        private readonly WorldVoxelIndex _sampleIndex;
 580        private readonly Vector3d _originWorldPosition;
 581        private int _minX;
 582        private int _minY;
 583        private int _minZ;
 584        private int _maxX;
 585        private int _maxY;
 586        private int _maxZ;
 587
 588        public int Count { get; private set; }
 589
 122590        public FlowFieldSamplingGridBuilder(
 122591            WorldVoxelIndex sampleIndex,
 122592            Vector3d sampleWorldPosition,
 122593            Fixed64 voxelSize)
 594        {
 122595            _sampleIndex = sampleIndex;
 122596            VoxelIndex localIndex = sampleIndex.VoxelIndex;
 122597            _originWorldPosition = new Vector3d(
 122598                sampleWorldPosition.x - (voxelSize * (Fixed64)localIndex.x),
 122599                sampleWorldPosition.y - (voxelSize * (Fixed64)localIndex.y),
 122600                sampleWorldPosition.z - (voxelSize * (Fixed64)localIndex.z));
 122601        }
 602
 603        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 604        public bool MatchesGrid(WorldVoxelIndex index)
 605        {
 1694606            return index.WorldSpawnToken == _sampleIndex.WorldSpawnToken
 1694607                && index.GridIndex == _sampleIndex.GridIndex
 1694608                && index.GridSpawnToken == _sampleIndex.GridSpawnToken;
 609        }
 610
 611        public void Include(WorldVoxelIndex index)
 612        {
 1816613            VoxelIndex localIndex = index.VoxelIndex;
 1816614            if (Count == 0)
 615            {
 122616                _minX = _maxX = localIndex.x;
 122617                _minY = _maxY = localIndex.y;
 122618                _minZ = _maxZ = localIndex.z;
 619            }
 620            else
 621            {
 1967622                if (localIndex.x < _minX) _minX = localIndex.x;
 1698623                if (localIndex.y < _minY) _minY = localIndex.y;
 1838624                if (localIndex.z < _minZ) _minZ = localIndex.z;
 1730625                if (localIndex.x > _maxX) _maxX = localIndex.x;
 1696626                if (localIndex.y > _maxY) _maxY = localIndex.y;
 1746627                if (localIndex.z > _maxZ) _maxZ = localIndex.z;
 628            }
 629
 1816630            Count++;
 1816631        }
 632
 633        public FlowFieldSamplingGrid Create(Fixed64 voxelSize)
 634        {
 122635            return new FlowFieldSamplingGrid(
 122636                _sampleIndex,
 122637                _originWorldPosition,
 122638                voxelSize,
 122639                _minX,
 122640                _minY,
 122641                _minZ,
 122642                _maxX,
 122643                _maxY,
 122644                _maxZ,
 122645                Count);
 646        }
 647    }
 648}

Methods/Properties

.cctor()
get_Shared()
.ctor()
FindPath(Trailblazer.Pathing.FlowFieldPathRequest)
FloodPath()
AnalyzeNeighborDistance(Trailblazer.Pathing.SolidChartPartition,System.Int32)
TryProcessDirection(Trailblazer.Pathing.SolidChartPartition,GridForge.Spatial.SpatialDirection[],System.Int32,System.Boolean)
ExceedsMaxClimbHeight(Trailblazer.Pathing.SolidChartPartition,Trailblazer.Pathing.SolidChartPartition)
HasValidDiagonalLegs(Trailblazer.Pathing.SolidChartPartition,GridForge.Spatial.SpatialDirection)
IsLegClear(Trailblazer.Pathing.SolidChartPartition,GridForge.Spatial.SpatialDirection)
GenerateFlowFields()
AddFlowField(SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>,Trailblazer.Pathing.FlowFieldSamplingGrid,GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField)
GetPathCost(Trailblazer.Pathing.SolidChartPartition)
GetPathCostTotal(Trailblazer.Pathing.SolidChartPartition)
ClearWorkingState()
PrepareSamplingGrids()
GetOrAddSamplingGridBuilder(GridForge.Spatial.WorldVoxelIndex,FixedMathSharp.Vector3d)
GetSamplingGrid(GridForge.Spatial.WorldVoxelIndex)
CreateNormalizedDirectionLookup()
SampleFlowVector(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,Trailblazer.Pathing.FlowFieldSurveyResult)
SampleFlowVector(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>,Trailblazer.Pathing.FlowFieldSamplingGrid[])
TryGetNearestFlowAnchor(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>,FixedMathSharp.Fixed64,GridForge.Grids.Voxel&)
GetFlowDirection(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>)
GetFlowDirection(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>,Trailblazer.Pathing.FlowFieldSamplingGrid[])
GetFlowField(Trailblazer.TrailblazerWorldContext,FixedMathSharp.Vector3d,SwiftCollections.SwiftDictionary`2<GridForge.Spatial.WorldVoxelIndex,Trailblazer.Pathing.FlowField>)
ThrowIfResultOwnedByDifferentContext(Trailblazer.Pathing.FlowFieldSurveyResult,Trailblazer.TrailblazerWorldContext)
.ctor(GridForge.Spatial.WorldVoxelIndex,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64)
MatchesGrid(GridForge.Spatial.WorldVoxelIndex)
Include(GridForge.Spatial.WorldVoxelIndex)
Create(FixedMathSharp.Fixed64)