< Summary

Information
Class: Trailblazer.Pathing.NavigationResolvedEndpoint
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/AStar/NavigationEndpointResolutionWork.cs
Line coverage
100%
Covered lines: 7
Uncovered lines: 0
Coverable lines: 7
Total lines: 722
Line coverage: 100%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Search/AStar/NavigationEndpointResolutionWork.cs

#LineLine coverage
 1//=======================================================================
 2// NavigationEndpointResolutionWork.cs
 3//=======================================================================
 4// MIT License, Copyright (c) 2024-present David Oravsky (mrdav30)
 5// See LICENSE file in the project root for full license information.
 6//=======================================================================
 7
 8using System;
 9using FixedMathSharp;
 10using GridForge.Grids;
 11using GridForge.Grids.Topology;
 12
 13namespace Trailblazer.Pathing;
 14
 15/// <summary>Reports bounded endpoint-resolution progress.</summary>
 16internal enum NavigationEndpointResolutionStatus : byte
 17{
 18    Pending = 0,
 19    Success = 1,
 20    NoMap = 2,
 21    InvalidEndpoint = 3,
 22    BudgetExceeded = 4,
 23    CostOverflow = 5,
 24    CapacityExceeded = 6,
 25    Stale = 7
 26}
 27
 28/// <summary>Identifies the directed query endpoint being resolved.</summary>
 29internal enum NavigationEndpointRole : byte
 30{
 31    Start,
 32    Destination
 33}
 34
 35/// <summary>Identifies one exact resolved graph endpoint.</summary>
 36internal readonly struct NavigationResolvedEndpoint
 37{
 38    internal NavigationResolvedEndpoint(
 39        NavigationNodeRef node,
 40        NavigationCellAddress address,
 41        TraversalMedia media,
 42        TraversalMedium resolutionMedium,
 43        Vector3d footAnchor,
 44        Fixed64 resolutionDistance)
 45    {
 300746        Node = node;
 300747        Address = address;
 300748        Media = media;
 300749        ResolutionMedium = resolutionMedium;
 300750        FootAnchor = footAnchor;
 300751        ResolutionDistance = resolutionDistance;
 300752    }
 53
 54    internal NavigationNodeRef Node { get; }
 55    internal NavigationCellAddress Address { get; }
 56    internal TraversalMedia Media { get; }
 57    internal TraversalMedium ResolutionMedium { get; }
 58    internal Vector3d FootAnchor { get; }
 59    internal Fixed64 ResolutionDistance { get; }
 60}
 61
 62/// <summary>Resolves one endpoint through bounded GridForge coverage and immutable graph filtering.</summary>
 63internal sealed class NavigationEndpointResolutionWork
 64{
 65    private readonly GridWorld _world;
 66    private readonly NavigationWorldGraphStore _store;
 67    private readonly NavigationWorkMeter _meter;
 68    private readonly NavigationEndpointWorkspace _workspace;
 69    private readonly NavigationRayWorkspace _rayWorkspace;
 70    private readonly NavigationRayWork _rayWork;
 71    private NavigationWorldGraph _graph = null!;
 72    private NavigationEndpoint _endpoint;
 73    private TraversalEvaluator _evaluator;
 74    private NavigationVolumeAnchorEvaluator _volumeEvaluator;
 75    private TraversalMedia _media;
 76    private NavigationEndpointRole _role;
 77    private NavigationResolvedEndpoint _pendingCandidate;
 78    private int _mapOrdinal;
 79    private int _generationInputOrdinal;
 80    private ulong _worldChangeSequence;
 81    private bool _discoveryComplete;
 82    private bool _cursorBegun;
 83    private bool _cursorComplete;
 84    private bool _hasResult;
 85    private bool _requiresWorldStamp;
 86
 87    internal NavigationEndpointResolutionWork(
 88        GridWorld world,
 89        NavigationWorldGraphStore store,
 90        NavigationWorkMeter meter,
 91        NavigationEndpointWorkspace workspace,
 92        NavigationRayWorkspace rayWorkspace,
 93        NavigationRayWork rayWork)
 94    {
 95        SwiftThrowHelper.ThrowIfNull(world, nameof(world));
 96        SwiftThrowHelper.ThrowIfNull(store, nameof(store));
 97        SwiftThrowHelper.ThrowIfNull(meter, nameof(meter));
 98        SwiftThrowHelper.ThrowIfNull(workspace, nameof(workspace));
 99        SwiftThrowHelper.ThrowIfNull(rayWorkspace, nameof(rayWorkspace));
 100        SwiftThrowHelper.ThrowIfNull(rayWork, nameof(rayWork));
 101        _world = world;
 102        _store = store;
 103        _meter = meter;
 104        _workspace = workspace;
 105        _rayWorkspace = rayWorkspace;
 106        _rayWork = rayWork;
 107    }
 108
 109    internal void Begin(
 110        NavigationWorldGraph graph,
 111        NavigationEndpoint endpoint,
 112        NavigationEndpointRole role,
 113        NavigationAgentProfile profile,
 114        NavigationAreaPolicy areaPolicy,
 115        TraversalMedia media)
 116    {
 117        SwiftThrowHelper.ThrowIfNull(graph, nameof(graph));
 118        SwiftThrowHelper.ThrowIfNull(areaPolicy, nameof(areaPolicy));
 119        SwiftThrowHelper.ThrowIfArgument(
 120            role is not NavigationEndpointRole.Start
 121                and not NavigationEndpointRole.Destination,
 122            nameof(role),
 123            "Endpoint role must be start or destination.");
 124        _graph = graph;
 125        _endpoint = endpoint;
 126        _role = role;
 127        _media = media;
 128        _evaluator = new TraversalEvaluator(
 129            graph,
 130            profile,
 131            areaPolicy,
 132            TraversalMedium.Solid);
 133        _volumeEvaluator = new NavigationVolumeAnchorEvaluator(
 134            _world,
 135            graph,
 136            profile,
 137            areaPolicy,
 138            _rayWorkspace);
 139        _pendingCandidate = default;
 140        _mapOrdinal = 0;
 141        _generationInputOrdinal = 0;
 142        _worldChangeSequence = _world.ChangeSequence;
 143        _cursorBegun = false;
 144        _cursorComplete = false;
 145        _hasResult = false;
 146        _requiresWorldStamp = false;
 147        Result = default;
 148        _rayWork.Reset();
 149        _workspace.ResetResolution();
 150        _discoveryComplete = endpoint.MapId == null && graph.MapCount == 0;
 151        Status = (endpoint.MapId == null
 152                && graph.MapCount > _workspace.CoveredAddressGenerations.Length)
 153            || (endpoint.MapId != null
 154                && _workspace.CoveredAddressGenerations.Length == 0)
 155                ? NavigationEndpointResolutionStatus.CapacityExceeded
 156                : NavigationEndpointResolutionStatus.Pending;
 157    }
 158
 159    internal void Reset()
 160    {
 161        _graph = null!;
 162        _endpoint = default;
 163        _evaluator = default;
 164        _volumeEvaluator = default;
 165        _media = default;
 166        _role = default;
 167        _pendingCandidate = default;
 168        _mapOrdinal = 0;
 169        _generationInputOrdinal = 0;
 170        _worldChangeSequence = 0;
 171        _discoveryComplete = false;
 172        _cursorBegun = false;
 173        _cursorComplete = false;
 174        _hasResult = false;
 175        _requiresWorldStamp = false;
 176        _rayWork.Reset();
 177        Status = default;
 178        Result = default;
 179    }
 180
 181    internal NavigationEndpointResolutionStatus Status { get; private set; }
 182
 183    internal NavigationResolvedEndpoint Result { get; private set; }
 184
 185    internal bool RequiresWorldStamp => _requiresWorldStamp;
 186
 187    internal NavigationEndpointResolutionStatus Advance(
 188        int lookupStepLimit,
 189        int endpointCandidateStepLimit)
 190    {
 191        SwiftThrowHelper.ThrowIfNegative(lookupStepLimit, nameof(lookupStepLimit));
 192        SwiftThrowHelper.ThrowIfNegative(
 193            endpointCandidateStepLimit,
 194            nameof(endpointCandidateStepLimit));
 195        if (Status != NavigationEndpointResolutionStatus.Pending)
 196            return Status;
 197
 198        if (_pendingCandidate.Node.IsValid)
 199        {
 200            NavigationEndpointResolutionStatus candidateStatus = AdvanceCandidateRay();
 201            if (candidateStatus != NavigationEndpointResolutionStatus.Pending)
 202                return candidateStatus;
 203            return Status;
 204        }
 205        if (_cursorComplete)
 206        {
 207            return Finish(NavigationSearchFinalizationRules.ResolveEndpointCursorStatus(
 208                _hasResult));
 209        }
 210
 211        int lookupRemaining = Math.Min(lookupStepLimit, _meter.RemainingLookupProbes);
 212        int candidateRemaining = Math.Min(
 213            endpointCandidateStepLimit,
 214            _meter.RemainingEndpointCandidates);
 215        if (!_discoveryComplete)
 216        {
 217            if (lookupRemaining == 0)
 218            {
 219                return _meter.RemainingLookupProbes == 0
 220                    ? Finish(NavigationEndpointResolutionStatus.BudgetExceeded)
 221                    : Status;
 222            }
 223            DiscoverGeneration();
 224            _meter.TryConsumeLookupProbes(1);
 225            lookupRemaining--;
 226            if (!_discoveryComplete || lookupRemaining == 0)
 227                return Status;
 228        }
 229
 230        if (!_cursorBegun)
 231        {
 232            if (_workspace.CoveredAddressGenerationCount == 0)
 233                return Finish(NavigationEndpointResolutionStatus.NoMap);
 234            if (!TryGetBounds(out Vector3d minimum, out Vector3d maximum))
 235                return Finish(NavigationEndpointResolutionStatus.CostOverflow);
 236            // Generation storage and the cursor are constructed from the same map capacity,
 237            // and Begin rejects a graph whose count exceeds that storage before discovery.
 238            _world.TryBeginCoveredAddresses(
 239                _workspace.CoveredAddressCursor,
 240                minimum,
 241                maximum,
 242                _workspace.CoveredAddressGenerationCount);
 243            _cursorBegun = true;
 244        }
 245
 246        while (lookupRemaining > 0 || candidateRemaining > 0)
 247        {
 248            bool binding = _generationInputOrdinal
 249                < _workspace.CoveredAddressGenerationCount;
 250            ReadOnlySpan<GridCoveredAddressGeneration> input = binding
 251                ? _workspace.CoveredAddressGenerations.AsSpan(
 252                    _generationInputOrdinal,
 253                    _workspace.CoveredAddressGenerationCount - _generationInputOrdinal)
 254                : ReadOnlySpan<GridCoveredAddressGeneration>.Empty;
 255            GridCoveredAddressCursorStatus cursorStatus = _world.AdvanceCoveredAddresses(
 256                _workspace.CoveredAddressCursor,
 257                input,
 258                _workspace.CoveredAddressOutput,
 259                lookupProbeLimit: binding ? lookupRemaining : 0,
 260                addressProbeLimit: binding ? 0 : lookupRemaining,
 261                outputLimit: candidateRemaining > 0 ? 1 : 0,
 262                out int lookupProbes,
 263                out int addressProbes,
 264                out int inputsConsumed,
 265                out int outputCount);
 266            _cursorComplete = cursorStatus == GridCoveredAddressCursorStatus.Complete;
 267            int consumedLookup = checked(lookupProbes + addressProbes);
 268            _meter.TryConsumeLookupProbes(consumedLookup);
 269            lookupRemaining -= consumedLookup;
 270            _generationInputOrdinal += inputsConsumed;
 271            if (cursorStatus == GridCoveredAddressCursorStatus.Stale)
 272                return Finish(NavigationEndpointResolutionStatus.Stale);
 273            if (outputCount != 0)
 274            {
 275                _meter.TryConsumeEndpointCandidates(1);
 276                candidateRemaining--;
 277                if (!ConsiderCandidate(_workspace.CoveredAddressOutput[0]))
 278                    return Status;
 279            }
 280            if (cursorStatus == GridCoveredAddressCursorStatus.Complete)
 281            {
 282                return Finish(NavigationSearchFinalizationRules.ResolveEndpointCursorStatus(
 283                    _hasResult));
 284            }
 285            if (consumedLookup == 0 && outputCount == 0)
 286                break;
 287        }
 288
 289        if (_meter.RemainingLookupProbes == 0
 290            || (_generationInputOrdinal >= _workspace.CoveredAddressGenerationCount
 291                && _meter.RemainingEndpointCandidates == 0))
 292        {
 293            return Finish(NavigationEndpointResolutionStatus.BudgetExceeded);
 294        }
 295        return Status;
 296    }
 297
 298    private void DiscoverGeneration()
 299    {
 300        if (_endpoint.MapId != null)
 301        {
 302            if (_graph.TryGetCoveredAddressGeneration(
 303                    _endpoint.MapId,
 304                    out GridCoveredAddressGeneration generation))
 305            {
 306                _workspace.CoveredAddressGenerations[0] = generation;
 307                _workspace.CoveredAddressGenerationCount = 1;
 308            }
 309            _mapOrdinal = 1;
 310            _discoveryComplete = true;
 311            return;
 312        }
 313
 314        // This method is called only while discovery is incomplete, so the ordinal is in range.
 315        _graph.TryGetCoveredAddressGeneration(
 316            _mapOrdinal,
 317            out _,
 318            out GridCoveredAddressGeneration generationAtOrdinal);
 319        _workspace.CoveredAddressGenerations[
 320            _workspace.CoveredAddressGenerationCount++] = generationAtOrdinal;
 321        _mapOrdinal++;
 322        _discoveryComplete = _mapOrdinal >= _graph.MapCount;
 323    }
 324
 325    private bool ConsiderCandidate(GridCoveredAddress candidate)
 326    {
 327        // Covered generations originate from this graph's configuration index.
 328        _graph.TryGetMapId(candidate.ConfigurationKey, out string mapId);
 329        if (!_graph.TryGetNodeRef(
 330                new NavigationCellAddress(mapId, candidate.VoxelIndex),
 331                out NavigationNodeRef node))
 332        {
 333            return true;
 334        }
 335        if (!_workspace.TryRecordPage(
 336                mapId,
 337                node.CellSlot / NavigationSemanticPage.SlotCount))
 338        {
 339            Finish(NavigationEndpointResolutionStatus.CapacityExceeded);
 340            return false;
 341        }
 342        bool consumedVolumeWork = false;
 343        _graph.TryGetNodeState(node, out NavigationNodeState state);
 344        bool qualified = TryQualifyCandidate(
 345                node,
 346                state,
 347                out TraversalMedia qualifyingMedia,
 348                out TraversalMedium resolutionMedium,
 349                out Vector3d footAnchor,
 350                out Fixed64 distance,
 351                out consumedVolumeWork);
 352        if (consumedVolumeWork)
 353            _requiresWorldStamp = true;
 354        if (!qualified)
 355            return Status == NavigationEndpointResolutionStatus.Pending
 356                && !consumedVolumeWork;
 357        var address = new NavigationCellAddress(mapId, candidate.VoxelIndex);
 358        var resolved = new NavigationResolvedEndpoint(
 359            node,
 360            address,
 361            qualifyingMedia,
 362            resolutionMedium,
 363            footAnchor,
 364            distance);
 365        if (!SelectPreferredCandidate(
 366                _hasResult,
 367                Result,
 368                resolved,
 369                out _))
 370        {
 371            return true;
 372        }
 373        if (_endpoint.Resolution == EndpointResolutionPolicy.Strict
 374            || (qualifyingMedia & TraversalMedia.Solid) == 0)
 375        {
 376            AcceptCandidate(resolved);
 377            return _endpoint.Resolution == EndpointResolutionPolicy.Strict
 378                && resolutionMedium == TraversalMedium.Solid;
 379        }
 380
 381        _pendingCandidate = resolved;
 382        Vector3d start = _role == NavigationEndpointRole.Start
 383            ? _endpoint.Position
 384            : state.FootAnchor;
 385        Vector3d end = _role == NavigationEndpointRole.Start
 386            ? state.FootAnchor
 387            : _endpoint.Position;
 388        NavigationRayChainConstraint constraint = _role == NavigationEndpointRole.Start
 389            ? NavigationRayChainConstraint.FinishAt(address)
 390            : NavigationRayChainConstraint.SeedAt(address);
 391        _rayWork.Begin(new NavigationRayRequest(
 392            _world,
 393            _store,
 394            _graph,
 395            _evaluator.Profile,
 396            _evaluator.AreaPolicy,
 397            TraversalMedium.Solid,
 398            start,
 399            end,
 400            _role == NavigationEndpointRole.Start
 401                ? NavigationRayEndpointAllowance.StartPrefix
 402                : NavigationRayEndpointAllowance.DestinationSuffix,
 403            constraint));
 404        return false;
 405    }
 406
 407    private NavigationEndpointResolutionStatus AdvanceCandidateRay()
 408    {
 409        NavigationRayStatus rayStatus = _rayWork.Advance(_meter);
 410        if (rayStatus is NavigationRayStatus.Success or NavigationRayStatus.Blocked)
 411        {
 412            if (!TryMergeRayDependencies())
 413                return Finish(NavigationEndpointResolutionStatus.CapacityExceeded);
 414            if (rayStatus == NavigationRayStatus.Success)
 415                AcceptCandidate(_pendingCandidate);
 416            else
 417                TryAcceptPendingVolumeFallback();
 418            _pendingCandidate = default;
 419            _rayWork.Reset();
 420            return Status;
 421        }
 422        return Finish((NavigationEndpointResolutionStatus)((byte)rayStatus + 1));
 423    }
 424
 425    private bool TryMergeRayDependencies()
 426    {
 427        NavigationDependencyWorkspace dependencies = _rayWorkspace.Dependencies;
 428        for (int i = 0; i < dependencies.ComponentCount; i++)
 429        {
 430            if (!_workspace.TryRecordComponent(dependencies.Components[i]))
 431                return false;
 432        }
 433        for (int i = 0; i < dependencies.PageCount; i++)
 434        {
 435            GraphPageDependencyAddress page = dependencies.Pages[i];
 436            if (!_workspace.TryRecordPage(page.MapId, page.PageIndex))
 437                return false;
 438        }
 439        return true;
 440    }
 441
 442    private void TryAcceptPendingVolumeFallback()
 443    {
 444        TraversalMedia volumeMedia = _pendingCandidate.Media & TraversalMedia.AnyVolume;
 445        if (volumeMedia == TraversalMedia.None)
 446            return;
 447        _graph.TryGetNodeState(
 448            _pendingCandidate.Node,
 449            out NavigationNodeState state);
 450        state.TryGetCenteredVolumeFootAnchor(
 451            _evaluator.Profile.Shape.Height,
 452            out Vector3d footAnchor);
 453        _ = Vector3d.TryGetDistance(
 454            _endpoint.Position,
 455            footAnchor,
 456            out Fixed64 distance);
 457
 458        TraversalMedium medium = FirstVolumeMedium(volumeMedia);
 459        var candidate = new NavigationResolvedEndpoint(
 460            _pendingCandidate.Node,
 461            _pendingCandidate.Address,
 462            volumeMedia,
 463            medium,
 464            footAnchor,
 465            distance);
 466        SelectPreferredCandidate(
 467            _hasResult,
 468            Result,
 469            candidate,
 470            out NavigationResolvedEndpoint preferred);
 471        AcceptCandidate(preferred);
 472    }
 473
 474    internal static bool SelectPreferredCandidate(
 475        bool hasCurrent,
 476        NavigationResolvedEndpoint current,
 477        NavigationResolvedEndpoint candidate,
 478        out NavigationResolvedEndpoint preferred)
 479    {
 480        bool selectCandidate = !hasCurrent
 481            || candidate.ResolutionDistance < current.ResolutionDistance
 482            || (candidate.ResolutionDistance == current.ResolutionDistance
 483                && candidate.Address.CompareTo(current.Address) < 0);
 484        preferred = selectCandidate ? candidate : current;
 485        return selectCandidate;
 486    }
 487
 488    private void AcceptCandidate(NavigationResolvedEndpoint candidate)
 489    {
 490        Result = candidate;
 491        _hasResult = true;
 492    }
 493
 494    private bool TryGetBounds(out Vector3d minimum, out Vector3d maximum)
 495    {
 496        Fixed64 distance = _endpoint.Resolution == EndpointResolutionPolicy.Strict
 497            ? Fixed64.Zero
 498            : _endpoint.MaxResolutionDistance;
 499        Fixed64 minX = default;
 500        Fixed64 minY = default;
 501        Fixed64 minZ = default;
 502        Fixed64 maxX = default;
 503        Fixed64 maxY = default;
 504        Fixed64 maxZ = default;
 505        bool boundsValid = Fixed64.TrySubtract(
 506                _endpoint.Position.X,
 507                distance,
 508                out minX)
 509            && Fixed64.TrySubtract(_endpoint.Position.Y, distance, out minY)
 510            && Fixed64.TrySubtract(_endpoint.Position.Z, distance, out minZ)
 511            && Fixed64.TryAdd(_endpoint.Position.X, distance, out maxX)
 512            && Fixed64.TryAdd(_endpoint.Position.Y, distance, out maxY)
 513            && Fixed64.TryAdd(_endpoint.Position.Z, distance, out maxZ);
 514        if (!boundsValid)
 515        {
 516            minimum = default;
 517            maximum = default;
 518            return false;
 519        }
 520        minimum = new Vector3d(minX, minY, minZ);
 521        maximum = new Vector3d(maxX, maxY, maxZ);
 522        return true;
 523    }
 524
 525    private NavigationEndpointResolutionStatus Finish(
 526        NavigationEndpointResolutionStatus status)
 527    {
 528        _pendingCandidate = default;
 529        _rayWork.Reset();
 530        if (status == NavigationEndpointResolutionStatus.Success)
 531        {
 532            status = RecordResultComponents();
 533        }
 534        bool dependenciesWereRead = status == NavigationEndpointResolutionStatus.Success
 535            || (status == NavigationEndpointResolutionStatus.InvalidEndpoint
 536                && (_workspace.PageCount != 0 || _workspace.ComponentCount != 0));
 537        bool epochWasCurrent = NavigationSearchFinalizationRules.IsEpochCurrent(
 538            epochRequired: true,
 539            expectedEpoch: _worldChangeSequence,
 540            currentEpoch: _world.ChangeSequence);
 541        bool dependenciesAreCurrent = !dependenciesWereRead
 542            || (epochWasCurrent && AreDependenciesCurrent());
 543        // Dependency validation can observe several immutable records, so recheck the
 544        // world epoch before publishing the terminal result.
 545        bool epochIsCurrent = epochWasCurrent
 546            && NavigationSearchFinalizationRules.IsEpochCurrent(
 547                epochRequired: true,
 548                expectedEpoch: _worldChangeSequence,
 549                currentEpoch: _world.ChangeSequence);
 550        status = NavigationSearchFinalizationRules.ResolveEndpointStatus(
 551            status,
 552            dependenciesWereRead,
 553            dependenciesAreCurrent,
 554            epochIsCurrent);
 555        Status = status;
 556        if (status != NavigationEndpointResolutionStatus.Success)
 557            Result = default;
 558        return Status;
 559    }
 560
 561    private bool AreDependenciesCurrent()
 562    {
 563        NavigationWorldGraph current = _store.Current;
 564        NavigationAreaPolicy areaPolicy = _evaluator.AreaPolicy;
 565        if (_graph.AreaCatalog.TryGet(
 566                areaPolicy.Key,
 567                out NavigationAreaPolicy expectedPolicy)
 568            && (!current.AreaCatalog.TryGet(
 569                    areaPolicy.Key,
 570                    out NavigationAreaPolicy currentPolicy)
 571                || !currentPolicy.ContentEquals(expectedPolicy)))
 572        {
 573            return false;
 574        }
 575        NavigationDependencyWorkspace dependencies = _workspace.Dependencies;
 576        for (int i = 0; i < dependencies.ComponentCount; i++)
 577        {
 578            NavigationSurfaceComponentKey key = dependencies.Components[i];
 579            _graph.TryGetComponentDependency(key, out GraphComponentDependency prior);
 580            if (!current.TryGetComponentDependency(key, out GraphComponentDependency next)
 581                || !prior.Equals(next))
 582            {
 583                return false;
 584            }
 585        }
 586        for (int i = 0; i < dependencies.PageCount; i++)
 587        {
 588            GraphPageDependencyAddress address = dependencies.Pages[i];
 589            _graph.TryGetPageDependency(address, out GraphPageDependency prior);
 590            if (!current.TryGetPageDependency(address, out GraphPageDependency next)
 591                || !prior.Equals(next))
 592            {
 593                return false;
 594            }
 595        }
 596        return true;
 597    }
 598
 599    private bool TryQualifyCandidate(
 600        NavigationNodeRef node,
 601        NavigationNodeState state,
 602        out TraversalMedia qualifyingMedia,
 603        out TraversalMedium resolutionMedium,
 604        out Vector3d footAnchor,
 605        out Fixed64 distance,
 606        out bool consumedVolumeWork)
 607    {
 608        qualifyingMedia = TraversalMedia.None;
 609        resolutionMedium = default;
 610        footAnchor = default;
 611        distance = default;
 612        consumedVolumeWork = false;
 613        if ((_media & TraversalMedia.Solid) != 0
 614            && _evaluator.TryGetPassableNodeState(node, out _))
 615        {
 616            if (!ConsiderMedium(
 617                    TraversalMedium.Solid,
 618                    state.FootAnchor,
 619                    ref qualifyingMedia,
 620                    ref resolutionMedium,
 621                    ref footAnchor,
 622                    ref distance))
 623            {
 624                Finish(NavigationEndpointResolutionStatus.CostOverflow);
 625                return false;
 626            }
 627        }
 628
 629        TraversalMedia requestedVolume = _media & TraversalMedia.AnyVolume;
 630        if (requestedVolume != TraversalMedia.None)
 631        {
 632            consumedVolumeWork = true;
 633            NavigationVolumeAnchorStatus volumeStatus = _volumeEvaluator.Evaluate(
 634                node,
 635                requestedVolume,
 636                _meter,
 637                _workspace.Dependencies,
 638                out Vector3d volumeFoot,
 639                out TraversalMedia volumeMedia);
 640            if (volumeStatus is NavigationVolumeAnchorStatus.BudgetExceeded
 641                or NavigationVolumeAnchorStatus.CostOverflow
 642                or NavigationVolumeAnchorStatus.CapacityExceeded
 643                or NavigationVolumeAnchorStatus.Stale)
 644            {
 645                Finish((NavigationEndpointResolutionStatus)((byte)volumeStatus + 2));
 646                return false;
 647            }
 648            for (TraversalMedium medium = TraversalMedium.Gas;
 649                 medium <= TraversalMedium.Liquid;
 650                 medium++)
 651            {
 652                if ((volumeMedia & NavigationCell.ToMedia(medium)) != 0
 653                    && !ConsiderMedium(
 654                        medium,
 655                        volumeFoot,
 656                        ref qualifyingMedia,
 657                        ref resolutionMedium,
 658                        ref footAnchor,
 659                        ref distance))
 660                {
 661                    Finish(NavigationEndpointResolutionStatus.CostOverflow);
 662                    return false;
 663                }
 664            }
 665        }
 666        return qualifyingMedia != TraversalMedia.None;
 667    }
 668
 669    private bool ConsiderMedium(
 670        TraversalMedium medium,
 671        Vector3d anchor,
 672        ref TraversalMedia qualifyingMedia,
 673        ref TraversalMedium resolutionMedium,
 674        ref Vector3d footAnchor,
 675        ref Fixed64 distance)
 676    {
 677        if (!Vector3d.TryGetDistance(_endpoint.Position, anchor, out Fixed64 candidateDistance))
 678            return false;
 679        if (_endpoint.Resolution == EndpointResolutionPolicy.NearestNavigable
 680            && candidateDistance > _endpoint.MaxResolutionDistance)
 681        {
 682            return true;
 683        }
 684        qualifyingMedia |= NavigationCell.ToMedia(medium);
 685        if (resolutionMedium == TraversalMedium.Unknown
 686            || candidateDistance < distance
 687            || (candidateDistance == distance && (int)medium < (int)resolutionMedium))
 688        {
 689            resolutionMedium = medium;
 690            footAnchor = anchor;
 691            distance = candidateDistance;
 692        }
 693        return true;
 694    }
 695
 696    private NavigationEndpointResolutionStatus RecordResultComponents()
 697    {
 698        for (TraversalMedium medium = TraversalMedium.Solid;
 699             medium <= TraversalMedium.Liquid;
 700             medium++)
 701        {
 702            if ((Result.Media & NavigationCell.ToMedia(medium)) == 0)
 703                continue;
 704            _graph.TryGetSurfaceComponent(
 705                Result.Address,
 706                medium,
 707                out NavigationSurfaceComponentKey componentKey,
 708                out _);
 709            if (!_workspace.TryRecordComponent(componentKey))
 710                return NavigationEndpointResolutionStatus.CapacityExceeded;
 711        }
 712        return NavigationEndpointResolutionStatus.Success;
 713    }
 714
 715    private static TraversalMedium FirstVolumeMedium(TraversalMedia media)
 716    {
 717        if ((media & TraversalMedia.Gas) != 0)
 718            return TraversalMedium.Gas;
 719        return TraversalMedium.Liquid;
 720    }
 721
 722}