| | | 1 | | //======================================================================= |
| | | 2 | | // NavigationVolumeEdgeEvaluator.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 | | |
| | | 8 | | using FixedMathSharp; |
| | | 9 | | using GridForge.Grids; |
| | | 10 | | using GridForge.Grids.Topology; |
| | | 11 | | using NavigationVolumeEdgeStatus = Trailblazer.Pathing.NavigationTraversalEvaluationStatus; |
| | | 12 | | |
| | | 13 | | namespace Trailblazer.Pathing; |
| | | 14 | | |
| | | 15 | | internal enum NavigationTraversalEvaluationStatus : byte |
| | | 16 | | { |
| | | 17 | | Passable = 0, |
| | | 18 | | Impassable = 1, |
| | | 19 | | BudgetExceeded = 2, |
| | | 20 | | CostOverflow = 3, |
| | | 21 | | CapacityExceeded = 4, |
| | | 22 | | Stale = 5 |
| | | 23 | | } |
| | | 24 | | |
| | | 25 | | /// <summary>Evaluates one centered volume-body movement without surface constraints.</summary> |
| | | 26 | | internal readonly struct NavigationVolumeEdgeEvaluator |
| | | 27 | | { |
| | | 28 | | private readonly TraversalEvaluator _nodes; |
| | | 29 | | private readonly GridWorld _world; |
| | | 30 | | private readonly NavigationRayWorkspace _workspace; |
| | | 31 | | |
| | | 32 | | internal NavigationVolumeEdgeEvaluator( |
| | | 33 | | GridWorld world, |
| | | 34 | | NavigationWorldGraph graph, |
| | | 35 | | NavigationAgentProfile profile, |
| | | 36 | | NavigationAreaPolicy areaPolicy, |
| | | 37 | | TraversalMedium medium, |
| | | 38 | | NavigationRayWorkspace workspace) |
| | | 39 | | { |
| | 1318 | 40 | | _nodes = new TraversalEvaluator(graph, profile, areaPolicy, medium); |
| | 1318 | 41 | | _world = world; |
| | 1318 | 42 | | _workspace = workspace; |
| | 1318 | 43 | | } |
| | | 44 | | |
| | | 45 | | internal NavigationVolumeEdgeStatus Evaluate( |
| | | 46 | | NavigationMediumStateRef source, |
| | | 47 | | NavigationMediumStateRef target, |
| | | 48 | | bool isPrimary, |
| | | 49 | | NavigationAutomaticSeamRef seam, |
| | | 50 | | bool hasSeam, |
| | | 51 | | NavigationWorkMeter meter, |
| | | 52 | | NavigationDependencyWorkspace dependencies, |
| | | 53 | | out Fixed64 cost) |
| | | 54 | | { |
| | 2660 | 55 | | cost = default; |
| | 2660 | 56 | | NavigationWorldGraph graph = _nodes.Graph; |
| | 2660 | 57 | | if (source.Medium != target.Medium) |
| | 1 | 58 | | return NavigationVolumeEdgeStatus.Stale; |
| | 2659 | 59 | | bool foundSourceAddress = graph.TryGetNodeAddress( |
| | 2659 | 60 | | source.Node, |
| | 2659 | 61 | | out NavigationCellAddress sourceAddress); |
| | 2659 | 62 | | bool foundTargetAddress = graph.TryGetNodeAddress( |
| | 2659 | 63 | | target.Node, |
| | 2659 | 64 | | out NavigationCellAddress targetAddress); |
| | 2659 | 65 | | bool foundSourcePrism = graph.TryGetSeamPrism( |
| | 2659 | 66 | | sourceAddress, |
| | 2659 | 67 | | out GridCellPrism sourcePrism); |
| | 2659 | 68 | | bool foundTargetPrism = graph.TryGetSeamPrism( |
| | 2659 | 69 | | targetAddress, |
| | 2659 | 70 | | out GridCellPrism targetPrism); |
| | | 71 | | System.Diagnostics.Debug.Assert( |
| | | 72 | | foundSourceAddress && foundTargetAddress && foundSourcePrism && foundTargetPrism, |
| | | 73 | | "Volume edge endpoints retain addresses and prisms in their immutable graph."); |
| | 2659 | 74 | | if (!dependencies.TryRecordPage( |
| | 2659 | 75 | | sourceAddress.MapId, |
| | 2659 | 76 | | source.Node.CellSlot / NavigationSemanticPage.SlotCount) |
| | 2659 | 77 | | || !dependencies.TryRecordPage( |
| | 2659 | 78 | | targetAddress.MapId, |
| | 2659 | 79 | | target.Node.CellSlot / NavigationSemanticPage.SlotCount)) |
| | | 80 | | { |
| | 2 | 81 | | return NavigationVolumeEdgeStatus.CapacityExceeded; |
| | | 82 | | } |
| | 2657 | 83 | | if (!_nodes.TryGetPassableNode( |
| | 2657 | 84 | | source.Node, |
| | 2657 | 85 | | out NavigationNodeState sourceState, |
| | 2657 | 86 | | out _) |
| | 2657 | 87 | | || !_nodes.TryGetPassableNode( |
| | 2657 | 88 | | target.Node, |
| | 2657 | 89 | | out NavigationNodeState targetState, |
| | 2657 | 90 | | out NavigationAreaRule targetRule)) |
| | | 91 | | { |
| | 132 | 92 | | return NavigationVolumeEdgeStatus.Impassable; |
| | | 93 | | } |
| | | 94 | | |
| | 2525 | 95 | | KinematicBodyShape shape = _nodes.Profile.Shape; |
| | 2525 | 96 | | if (!sourceState.TryGetCenteredVolumeFootAnchor( |
| | 2525 | 97 | | shape.Height, |
| | 2525 | 98 | | out Vector3d sourceAnchor) |
| | 2525 | 99 | | || !targetState.TryGetCenteredVolumeFootAnchor( |
| | 2525 | 100 | | shape.Height, |
| | 2525 | 101 | | out Vector3d targetAnchor)) |
| | | 102 | | { |
| | 1 | 103 | | return NavigationVolumeEdgeStatus.CostOverflow; |
| | | 104 | | } |
| | | 105 | | System.Diagnostics.Debug.Assert( |
| | | 106 | | !hasSeam |
| | | 107 | | || (seam.Pair != null |
| | | 108 | | && graph.AutomaticSeams.IsActive(seam) |
| | | 109 | | && sourceAddress.Equals(seam.Source) |
| | | 110 | | && targetAddress.Equals(seam.Destination) |
| | | 111 | | && seam.Portal.IsValid)); |
| | | 112 | | System.Diagnostics.Debug.Assert( |
| | | 113 | | !hasSeam |
| | | 114 | | || GridCellGeometry.TryCreateNavigationPortal( |
| | | 115 | | sourcePrism, |
| | | 116 | | targetPrism, |
| | | 117 | | out _)); |
| | 2524 | 118 | | NavigationVolumeEdgeStatus segmentStatus = CertifyResolvedSegment( |
| | 2524 | 119 | | source, |
| | 2524 | 120 | | target, |
| | 2524 | 121 | | sourcePrism, |
| | 2524 | 122 | | targetPrism, |
| | 2524 | 123 | | sourceAnchor, |
| | 2524 | 124 | | targetAnchor, |
| | 2524 | 125 | | isPrimary, |
| | 2524 | 126 | | meter, |
| | 2524 | 127 | | dependencies); |
| | 2524 | 128 | | if (segmentStatus != NavigationVolumeEdgeStatus.Passable) |
| | 50 | 129 | | return segmentStatus; |
| | 2474 | 130 | | if (!TryGetCost( |
| | 2474 | 131 | | sourceAnchor, |
| | 2474 | 132 | | targetAnchor, |
| | 2474 | 133 | | targetState.Cell.EnterCost, |
| | 2474 | 134 | | targetRule.AdditionalEnterCost, |
| | 2474 | 135 | | out Fixed64 total)) |
| | | 136 | | { |
| | 3 | 137 | | return NavigationVolumeEdgeStatus.CostOverflow; |
| | | 138 | | } |
| | | 139 | | |
| | 2471 | 140 | | cost = total; |
| | 2471 | 141 | | return NavigationVolumeEdgeStatus.Passable; |
| | | 142 | | } |
| | | 143 | | |
| | | 144 | | internal static bool TryGetCost( |
| | | 145 | | Vector3d sourceAnchor, |
| | | 146 | | Vector3d targetAnchor, |
| | | 147 | | Fixed64 targetEnterCost, |
| | | 148 | | Fixed64 additionalEnterCost, |
| | 2478 | 149 | | out Fixed64 total) => NavigationDistanceMath.TryCeiling( |
| | 2478 | 150 | | sourceAnchor, |
| | 2478 | 151 | | targetAnchor, |
| | 2478 | 152 | | out total) |
| | 2478 | 153 | | && Fixed64.TryAdd(total, targetEnterCost, out total) |
| | 2478 | 154 | | && Fixed64.TryAdd(total, additionalEnterCost, out total); |
| | | 155 | | |
| | | 156 | | internal NavigationVolumeEdgeStatus CertifyRaySegment( |
| | | 157 | | NavigationMediumStateRef source, |
| | | 158 | | NavigationMediumStateRef target, |
| | | 159 | | Vector3d sourceFoot, |
| | | 160 | | Vector3d targetFoot, |
| | | 161 | | NavigationWorkMeter meter, |
| | | 162 | | NavigationDependencyWorkspace dependencies) |
| | | 163 | | { |
| | 580 | 164 | | NavigationWorldGraph graph = _nodes.Graph; |
| | 580 | 165 | | if (source.Medium != target.Medium) |
| | 1 | 166 | | return NavigationVolumeEdgeStatus.Stale; |
| | 579 | 167 | | bool foundSourceAddress = graph.TryGetNodeAddress( |
| | 579 | 168 | | source.Node, |
| | 579 | 169 | | out NavigationCellAddress sourceAddress); |
| | 579 | 170 | | bool foundTargetAddress = graph.TryGetNodeAddress( |
| | 579 | 171 | | target.Node, |
| | 579 | 172 | | out NavigationCellAddress targetAddress); |
| | 579 | 173 | | bool foundSourcePrism = graph.TryGetSeamPrism( |
| | 579 | 174 | | sourceAddress, |
| | 579 | 175 | | out GridCellPrism sourcePrism); |
| | 579 | 176 | | bool foundTargetPrism = graph.TryGetSeamPrism( |
| | 579 | 177 | | targetAddress, |
| | 579 | 178 | | out GridCellPrism targetPrism); |
| | | 179 | | System.Diagnostics.Debug.Assert( |
| | | 180 | | foundSourceAddress && foundTargetAddress && foundSourcePrism && foundTargetPrism, |
| | | 181 | | "Certified volume-ray endpoints retain addresses and prisms in their immutable graph."); |
| | 579 | 182 | | if (!dependencies.TryRecordPage( |
| | 579 | 183 | | sourceAddress.MapId, |
| | 579 | 184 | | source.Node.CellSlot / NavigationSemanticPage.SlotCount) |
| | 579 | 185 | | || !dependencies.TryRecordPage( |
| | 579 | 186 | | targetAddress.MapId, |
| | 579 | 187 | | target.Node.CellSlot / NavigationSemanticPage.SlotCount)) |
| | | 188 | | { |
| | 1 | 189 | | return NavigationVolumeEdgeStatus.CapacityExceeded; |
| | | 190 | | } |
| | 578 | 191 | | if (!_nodes.TryGetPassableNode(source.Node, out _, out _) |
| | 578 | 192 | | || !_nodes.TryGetPassableNode(target.Node, out _, out _)) |
| | | 193 | | { |
| | 2 | 194 | | return NavigationVolumeEdgeStatus.Impassable; |
| | | 195 | | } |
| | | 196 | | |
| | 576 | 197 | | return CertifyResolvedSegment( |
| | 576 | 198 | | source, |
| | 576 | 199 | | target, |
| | 576 | 200 | | sourcePrism, |
| | 576 | 201 | | targetPrism, |
| | 576 | 202 | | sourceFoot, |
| | 576 | 203 | | targetFoot, |
| | 576 | 204 | | allowPortalFastPath: true, |
| | 576 | 205 | | meter, |
| | 576 | 206 | | dependencies); |
| | | 207 | | } |
| | | 208 | | |
| | | 209 | | private NavigationVolumeEdgeStatus CertifyResolvedSegment( |
| | | 210 | | NavigationMediumStateRef source, |
| | | 211 | | NavigationMediumStateRef target, |
| | | 212 | | GridCellPrism sourcePrism, |
| | | 213 | | GridCellPrism targetPrism, |
| | | 214 | | Vector3d sourceFoot, |
| | | 215 | | Vector3d targetFoot, |
| | | 216 | | bool allowPortalFastPath, |
| | | 217 | | NavigationWorkMeter meter, |
| | | 218 | | NavigationDependencyWorkspace dependencies) |
| | | 219 | | { |
| | 3100 | 220 | | KinematicBodyShape shape = _nodes.Profile.Shape; |
| | | 221 | | bool fastPath; |
| | 3100 | 222 | | if (source.Node.Equals(target.Node)) |
| | | 223 | | { |
| | 303 | 224 | | fastPath = GridCellGeometry.IsNavigationBodySegmentValid( |
| | 303 | 225 | | sourcePrism, |
| | 303 | 226 | | sourceFoot, |
| | 303 | 227 | | targetFoot, |
| | 303 | 228 | | shape.Radius, |
| | 303 | 229 | | shape.Height, |
| | 303 | 230 | | default, |
| | 303 | 231 | | default, |
| | 303 | 232 | | GridNavigationBodySegmentEndpointAllowance.None); |
| | | 233 | | } |
| | | 234 | | else |
| | | 235 | | { |
| | 2797 | 236 | | fastPath = allowPortalFastPath |
| | 2797 | 237 | | && GridCellGeometry.TryCreateNavigationPortal( |
| | 2797 | 238 | | sourcePrism, |
| | 2797 | 239 | | targetPrism, |
| | 2797 | 240 | | out GridNavigationPortal portal) |
| | 2797 | 241 | | && GridCellGeometry.TryGetNavigationPortalTraversalParameters( |
| | 2797 | 242 | | sourcePrism, |
| | 2797 | 243 | | targetPrism, |
| | 2797 | 244 | | portal, |
| | 2797 | 245 | | sourceFoot, |
| | 2797 | 246 | | targetFoot, |
| | 2797 | 247 | | shape.Radius, |
| | 2797 | 248 | | shape.Height, |
| | 2797 | 249 | | out Fixed64 sourceParameter, |
| | 2797 | 250 | | out Fixed64 targetParameter) |
| | 2797 | 251 | | && GridCellGeometry.IsNavigationBodySegmentValid( |
| | 2797 | 252 | | sourcePrism, |
| | 2797 | 253 | | sourceFoot, |
| | 2797 | 254 | | Vector3d.Lerp(sourceFoot, targetFoot, sourceParameter), |
| | 2797 | 255 | | shape.Radius, |
| | 2797 | 256 | | shape.Height, |
| | 2797 | 257 | | default, |
| | 2797 | 258 | | portal, |
| | 2797 | 259 | | GridNavigationBodySegmentEndpointAllowance.None) |
| | 2797 | 260 | | && GridCellGeometry.IsNavigationBodySegmentValid( |
| | 2797 | 261 | | targetPrism, |
| | 2797 | 262 | | Vector3d.Lerp(sourceFoot, targetFoot, targetParameter), |
| | 2797 | 263 | | targetFoot, |
| | 2797 | 264 | | shape.Radius, |
| | 2797 | 265 | | shape.Height, |
| | 2797 | 266 | | portal, |
| | 2797 | 267 | | default, |
| | 2797 | 268 | | GridNavigationBodySegmentEndpointAllowance.None); |
| | | 269 | | } |
| | 3100 | 270 | | if (fastPath) |
| | 1567 | 271 | | return NavigationVolumeEdgeStatus.Passable; |
| | | 272 | | |
| | 1533 | 273 | | var union = new NavigationVolumeAnchorEvaluator( |
| | 1533 | 274 | | _world, |
| | 1533 | 275 | | _nodes.Graph, |
| | 1533 | 276 | | _nodes.Profile, |
| | 1533 | 277 | | _nodes.AreaPolicy, |
| | 1533 | 278 | | _workspace); |
| | 1533 | 279 | | NavigationVolumeAnchorStatus status = union.EvaluateSegment( |
| | 1533 | 280 | | source, |
| | 1533 | 281 | | target, |
| | 1533 | 282 | | sourceFoot, |
| | 1533 | 283 | | targetFoot, |
| | 1533 | 284 | | meter, |
| | 1533 | 285 | | dependencies); |
| | 1533 | 286 | | return status switch |
| | 1533 | 287 | | { |
| | 1470 | 288 | | NavigationVolumeAnchorStatus.Success => NavigationVolumeEdgeStatus.Passable, |
| | 1533 | 289 | | NavigationVolumeAnchorStatus.BudgetExceeded => |
| | 9 | 290 | | NavigationVolumeEdgeStatus.BudgetExceeded, |
| | 1533 | 291 | | NavigationVolumeAnchorStatus.CostOverflow => |
| | 1 | 292 | | NavigationVolumeEdgeStatus.CostOverflow, |
| | 1533 | 293 | | NavigationVolumeAnchorStatus.CapacityExceeded => |
| | 2 | 294 | | NavigationVolumeEdgeStatus.CapacityExceeded, |
| | 4 | 295 | | NavigationVolumeAnchorStatus.Stale => NavigationVolumeEdgeStatus.Stale, |
| | 47 | 296 | | _ => NavigationVolumeEdgeStatus.Impassable |
| | 1533 | 297 | | }; |
| | | 298 | | } |
| | | 299 | | } |