< Summary

Information
Class: Trailblazer.Pathing.NavigationTransitionEdgeEvidence
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Graph/NavigationTransitionEdgeEvaluator.cs
Line coverage
100%
Covered lines: 4
Uncovered lines: 0
Coverable lines: 4
Total lines: 446
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/Graph/NavigationTransitionEdgeEvaluator.cs

#LineLine coverage
 1//=======================================================================
 2// NavigationTransitionEdgeEvaluator.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 FixedMathSharp;
 9using GridForge.Grids;
 10using GridForge.Grids.Topology;
 11using NavigationTransitionEdgeStatus = Trailblazer.Pathing.NavigationTraversalEvaluationStatus;
 12
 13namespace Trailblazer.Pathing;
 14
 15/// <summary>Preserves the exact cost and action endpoints selected for a transition.</summary>
 16internal readonly struct NavigationTransitionEdgeEvidence
 17{
 18    internal NavigationTransitionEdgeEvidence(
 19        Fixed64 cost,
 20        Vector3d sourceAction,
 21        Vector3d destinationAction)
 22    {
 14823        Cost = cost;
 14824        SourceAction = sourceAction;
 14825        DestinationAction = destinationAction;
 14826    }
 27
 28    internal Fixed64 Cost { get; }
 29
 30    internal Vector3d SourceAction { get; }
 31
 32    internal Vector3d DestinationAction { get; }
 33}
 34
 35/// <summary>Evaluates authored transition action legs and costs without endpoint travel.</summary>
 36internal readonly struct NavigationTransitionEdgeEvaluator
 37{
 38    private readonly NavigationWorldGraph _graph;
 39    private readonly NavigationAgentProfile _profile;
 40    private readonly NavigationAreaPolicy _areaPolicy;
 41    private readonly GridWorld _world;
 42    private readonly NavigationRayWorkspace _workspace;
 43
 44    internal NavigationTransitionEdgeEvaluator(
 45        GridWorld world,
 46        NavigationWorldGraph graph,
 47        NavigationAgentProfile profile,
 48        NavigationAreaPolicy areaPolicy,
 49        NavigationRayWorkspace workspace)
 50    {
 51        _graph = graph;
 52        _profile = profile;
 53        _areaPolicy = areaPolicy;
 54        _world = world;
 55        _workspace = workspace;
 56    }
 57
 58    internal NavigationTransitionEdgeStatus EvaluateDefinition(
 59        NavigationMediumStateRef source,
 60        NavigationPublishedTransition transition,
 61        NavigationWorkMeter meter,
 62        NavigationDependencyWorkspace dependencies,
 63        out NavigationMediumStateRef target,
 64        out NavigationTransitionEdgeEvidence evidence)
 65    {
 66        target = default;
 67        evidence = default;
 68        TraversalTransitionDefinition definition = transition.Definition;
 69        bool found = _graph.TryGetNodeAddress(
 70            source.Node,
 71            out NavigationCellAddress sourceAddress);
 72        System.Diagnostics.Debug.Assert(found,
 73            "Transition candidates are evaluated only from published graph nodes.");
 74        if (!sourceAddress.Equals(transition.SourceAddress)
 75            || source.Medium != definition.SourceMedium)
 76        {
 77            return NavigationTransitionEdgeStatus.Stale;
 78        }
 79        if (!_graph.TryGetNodeRef(
 80                definition.Destination,
 81                out NavigationNodeRef targetNode))
 82        {
 83            return NavigationTransitionEdgeStatus.Impassable;
 84        }
 85        target = new NavigationMediumStateRef(
 86            targetNode,
 87            definition.DestinationMedium);
 88        if (!dependencies.TryRecordPage(
 89                sourceAddress.MapId,
 90                source.Node.CellSlot / NavigationSemanticPage.SlotCount)
 91            || !dependencies.TryRecordPage(
 92                definition.Destination.MapId,
 93                target.Node.CellSlot / NavigationSemanticPage.SlotCount))
 94        {
 95            return NavigationTransitionEdgeStatus.CapacityExceeded;
 96        }
 97        if ((_profile.Capabilities & definition.RequiredCapabilities)
 98                != definition.RequiredCapabilities)
 99        {
 100            return NavigationTransitionEdgeStatus.Impassable;
 101        }
 102
 103        var sourceNodes = new TraversalEvaluator(
 104            _graph,
 105            _profile,
 106            _areaPolicy,
 107            source.Medium);
 108        var targetNodes = new TraversalEvaluator(
 109            _graph,
 110            _profile,
 111            _areaPolicy,
 112            target.Medium);
 113        if (!sourceNodes.TryGetPassableNode(
 114                source.Node,
 115                out NavigationNodeState sourceState,
 116                out _)
 117            || !targetNodes.TryGetPassableNode(
 118                target.Node,
 119                out NavigationNodeState targetState,
 120                out NavigationAreaRule targetRule))
 121        {
 122            return NavigationTransitionEdgeStatus.Impassable;
 123        }
 124        if (!TryGetAnchors(
 125                sourceState,
 126                source.Medium,
 127                targetState,
 128                target.Medium,
 129                out Vector3d sourceAnchor,
 130                out Vector3d targetAnchor))
 131        {
 132            return NavigationTransitionEdgeStatus.Impassable;
 133        }
 134
 135        Vector3d sourceAction = definition.HasSourcePointOverride
 136            ? definition.SourcePointOverride
 137            : sourceAnchor;
 138        Vector3d targetAction = definition.HasDestinationPointOverride
 139            ? definition.DestinationPointOverride
 140            : targetAnchor;
 141        NavigationTransitionEdgeStatus sourceLeg = EvaluateLeg(
 142            source,
 143            sourceAnchor,
 144            sourceAction,
 145            meter,
 146            dependencies);
 147        if (sourceLeg != NavigationTransitionEdgeStatus.Passable)
 148            return sourceLeg;
 149        NavigationTransitionEdgeStatus targetLeg = EvaluateLeg(
 150            target,
 151            targetAction,
 152            targetAnchor,
 153            meter,
 154            dependencies);
 155        if (targetLeg != NavigationTransitionEdgeStatus.Passable)
 156            return targetLeg;
 157
 158        if (!TryGetCost(
 159                sourceAnchor,
 160                sourceAction,
 161                definition.ActionCost,
 162                targetAction,
 163                targetAnchor,
 164                targetState.Cell.EnterCost,
 165                targetRule.AdditionalEnterCost,
 166                out Fixed64 total))
 167        {
 168            return NavigationTransitionEdgeStatus.CostOverflow;
 169        }
 170
 171        evidence = new NavigationTransitionEdgeEvidence(
 172            total,
 173            sourceAction,
 174            targetAction);
 175        return NavigationTransitionEdgeStatus.Passable;
 176    }
 177
 178    internal NavigationTransitionEdgeStatus EvaluateRule(
 179        NavigationMediumStateRef source,
 180        NavigationMediumStateRef target,
 181        TraversalTransitionRule rule,
 182        NavigationWorkMeter meter,
 183        NavigationDependencyWorkspace dependencies,
 184        out NavigationTransitionEdgeEvidence evidence)
 185    {
 186        evidence = default;
 187        bool foundSource = _graph.TryGetNodeAddress(
 188            source.Node,
 189            out NavigationCellAddress address);
 190        bool foundTarget = _graph.TryGetNodeAddress(
 191            target.Node,
 192            out NavigationCellAddress targetAddress);
 193        System.Diagnostics.Debug.Assert(foundSource && foundTarget,
 194            "Transition rule pairs contain only published graph nodes.");
 195        if ((rule.Scope == TraversalTransitionRuleScope.SameCell
 196                && !source.Node.Equals(target.Node))
 197            || source.Medium != rule.SourceMedium
 198            || target.Medium != rule.DestinationMedium
 199            || (_profile.Capabilities & rule.RequiredCapabilities)
 200                != rule.RequiredCapabilities)
 201        {
 202            return NavigationTransitionEdgeStatus.Impassable;
 203        }
 204        if (!dependencies.TryRecordPage(
 205                address.MapId,
 206                source.Node.CellSlot / NavigationSemanticPage.SlotCount)
 207            || !dependencies.TryRecordPage(
 208                targetAddress.MapId,
 209                target.Node.CellSlot / NavigationSemanticPage.SlotCount))
 210        {
 211            return NavigationTransitionEdgeStatus.CapacityExceeded;
 212        }
 213
 214        var sourceNodes = new TraversalEvaluator(
 215            _graph,
 216            _profile,
 217            _areaPolicy,
 218            source.Medium);
 219        var targetNodes = new TraversalEvaluator(
 220            _graph,
 221            _profile,
 222            _areaPolicy,
 223            target.Medium);
 224        if (!sourceNodes.TryGetPassableNodeState(
 225                source.Node,
 226                out NavigationNodeState sourceState)
 227            || !targetNodes.TryGetPassableNode(
 228                target.Node,
 229                out NavigationNodeState targetState,
 230                out NavigationAreaRule targetRule))
 231        {
 232            return NavigationTransitionEdgeStatus.Impassable;
 233        }
 234        if (!TryGetAnchors(
 235                sourceState,
 236                source.Medium,
 237                targetState,
 238                target.Medium,
 239                out Vector3d sourceAnchor,
 240                out Vector3d targetAnchor))
 241        {
 242            return NavigationTransitionEdgeStatus.Impassable;
 243        }
 244
 245        Vector3d sourceAction = sourceAnchor;
 246        Vector3d targetAction = targetAnchor;
 247        GridNavigationPortal portal = default;
 248        if (rule.Scope == TraversalTransitionRuleScope.PositiveFaceContact)
 249        {
 250            bool foundSourcePrism = _graph.TryGetSeamPrism(
 251                address,
 252                out GridCellPrism sourcePrism);
 253            bool foundTargetPrism = _graph.TryGetSeamPrism(
 254                targetAddress,
 255                out GridCellPrism targetPrism);
 256            System.Diagnostics.Debug.Assert(foundSourcePrism && foundTargetPrism,
 257                "Published transition endpoints belong to their bound grid prisms.");
 258            if (!GridCellGeometry.TryCreateNavigationPortal(
 259                    sourcePrism,
 260                    targetPrism,
 261                    out portal))
 262            {
 263                return NavigationTransitionEdgeStatus.Impassable;
 264            }
 265            if (!GridCellGeometry.TryGetNavigationPortalTraversalParameters(
 266                sourcePrism,
 267                targetPrism,
 268                portal,
 269                sourceAnchor,
 270                targetAnchor,
 271                _profile.Shape.Radius,
 272                _profile.Shape.Height,
 273                out Fixed64 sourceParameter,
 274                out Fixed64 targetParameter))
 275            {
 276                return NavigationTransitionEdgeStatus.Impassable;
 277            }
 278            sourceAction = Vector3d.Lerp(sourceAnchor, targetAnchor, sourceParameter);
 279            targetAction = Vector3d.Lerp(sourceAnchor, targetAnchor, targetParameter);
 280        }
 281        NavigationTransitionEdgeStatus sourceLeg = EvaluateLeg(
 282            source,
 283            sourceAnchor,
 284            sourceAction,
 285            meter,
 286            dependencies,
 287            outgoingPortal: portal);
 288        if (sourceLeg != NavigationTransitionEdgeStatus.Passable)
 289            return sourceLeg;
 290        NavigationTransitionEdgeStatus targetLeg = EvaluateLeg(
 291            target,
 292            targetAction,
 293            targetAnchor,
 294            meter,
 295            dependencies,
 296            incomingPortal: portal);
 297        if (targetLeg != NavigationTransitionEdgeStatus.Passable)
 298            return targetLeg;
 299        if (!TryGetCost(
 300                sourceAnchor,
 301                sourceAction,
 302                rule.ActionCost,
 303                targetAction,
 304                targetAnchor,
 305                targetState.Cell.EnterCost,
 306                targetRule.AdditionalEnterCost,
 307                out Fixed64 total))
 308        {
 309            return NavigationTransitionEdgeStatus.CostOverflow;
 310        }
 311
 312        evidence = new NavigationTransitionEdgeEvidence(
 313            total,
 314            sourceAction,
 315            targetAction);
 316        return NavigationTransitionEdgeStatus.Passable;
 317    }
 318
 319    internal static bool TryGetCost(
 320        Vector3d sourceAnchor,
 321        Vector3d sourceAction,
 322        Fixed64 actionCost,
 323        Vector3d targetAction,
 324        Vector3d targetAnchor,
 325        Fixed64 targetEnterCost,
 326        Fixed64 additionalEnterCost,
 327        out Fixed64 total) => NavigationDistanceMath.TryCeiling(
 328            sourceAnchor,
 329            sourceAction,
 330            out total)
 331        && Fixed64.TryAdd(total, actionCost, out total)
 332        && NavigationDistanceMath.TryCeiling(
 333            targetAction,
 334            targetAnchor,
 335            out Fixed64 targetDistance)
 336        && Fixed64.TryAdd(total, targetDistance, out total)
 337        && Fixed64.TryAdd(total, targetEnterCost, out total)
 338        && Fixed64.TryAdd(total, additionalEnterCost, out total);
 339
 340    private bool TryGetAnchors(
 341        NavigationNodeState source,
 342        TraversalMedium sourceMedium,
 343        NavigationNodeState target,
 344        TraversalMedium targetMedium,
 345        out Vector3d sourceAnchor,
 346        out Vector3d targetAnchor)
 347    {
 348        if (!TryGetAnchor(source, sourceMedium, out sourceAnchor))
 349        {
 350            targetAnchor = default;
 351            return false;
 352        }
 353        return TryGetAnchor(target, targetMedium, out targetAnchor);
 354    }
 355
 356    private bool TryGetAnchor(
 357        NavigationNodeState state,
 358        TraversalMedium medium,
 359        out Vector3d anchor)
 360    {
 361        if (medium == TraversalMedium.Solid)
 362        {
 363            anchor = state.FootAnchor;
 364            return true;
 365        }
 366        return state.TryGetCenteredVolumeFootAnchor(_profile.Shape.Height, out anchor);
 367    }
 368
 369    private NavigationTransitionEdgeStatus EvaluateLeg(
 370        NavigationMediumStateRef state,
 371        Vector3d start,
 372        Vector3d end,
 373        NavigationWorkMeter meter,
 374        NavigationDependencyWorkspace dependencies,
 375        GridNavigationPortal incomingPortal = default,
 376        GridNavigationPortal outgoingPortal = default)
 377    {
 378        if (start == end && state.Medium == TraversalMedium.Solid)
 379            return NavigationTransitionEdgeStatus.Passable;
 380        if (state.Medium == TraversalMedium.Solid)
 381        {
 382            _graph.TryGetNodeAddress(state.Node, out NavigationCellAddress address);
 383            _graph.TryGetSeamPrism(address, out GridCellPrism prism);
 384            System.Diagnostics.Debug.Assert(
 385                address.MapId != null && prism.FootprintVertexCount > 0);
 386            bool valid = GridCellGeometry.IsNavigationBodySegmentValid(
 387                prism,
 388                start,
 389                end,
 390                _profile.Shape.Radius,
 391                _profile.Shape.Height,
 392                incomingPortal,
 393                outgoingPortal,
 394                GridNavigationBodySegmentEndpointAllowance.None);
 395            System.Diagnostics.Debug.Assert(valid,
 396                "Published transition actions and computed portals are valid in-cell segments.");
 397            return NavigationTransitionEdgeStatus.Passable;
 398        }
 399
 400        if ((incomingPortal.IsValid || outgoingPortal.IsValid)
 401            && _graph.TryGetNodeAddress(
 402                state.Node,
 403                out NavigationCellAddress volumeAddress)
 404            && _graph.TryGetSeamPrism(volumeAddress, out GridCellPrism volumePrism)
 405            && GridCellGeometry.IsNavigationBodySegmentValid(
 406                volumePrism,
 407                start,
 408                end,
 409                _profile.Shape.Radius,
 410                _profile.Shape.Height,
 411                incomingPortal,
 412                outgoingPortal,
 413                GridNavigationBodySegmentEndpointAllowance.None))
 414        {
 415            return NavigationTransitionEdgeStatus.Passable;
 416        }
 417
 418        var volume = new NavigationVolumeAnchorEvaluator(
 419            _world,
 420            _graph,
 421            _profile,
 422            _areaPolicy,
 423            _workspace);
 424        NavigationVolumeAnchorStatus status = volume.EvaluateSegment(
 425            state,
 426            state,
 427            start,
 428            end,
 429            meter,
 430            dependencies);
 431        return status switch
 432        {
 433            NavigationVolumeAnchorStatus.Success =>
 434                NavigationTransitionEdgeStatus.Passable,
 435            NavigationVolumeAnchorStatus.BudgetExceeded =>
 436                NavigationTransitionEdgeStatus.BudgetExceeded,
 437            NavigationVolumeAnchorStatus.CostOverflow =>
 438                NavigationTransitionEdgeStatus.CostOverflow,
 439            NavigationVolumeAnchorStatus.CapacityExceeded =>
 440                NavigationTransitionEdgeStatus.CapacityExceeded,
 441            NavigationVolumeAnchorStatus.Stale =>
 442                NavigationTransitionEdgeStatus.Stale,
 443            _ => NavigationTransitionEdgeStatus.Impassable
 444        };
 445    }
 446}