| | | 1 | | //======================================================================= |
| | | 2 | | // NavigationVolumeAnchorEvaluator.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 System; |
| | | 9 | | using FixedMathSharp; |
| | | 10 | | using GridForge.Grids; |
| | | 11 | | using GridForge.Spatial; |
| | | 12 | | using GridForge.Utility; |
| | | 13 | | |
| | | 14 | | namespace Trailblazer.Pathing; |
| | | 15 | | |
| | | 16 | | internal enum NavigationVolumeAnchorStatus : byte |
| | | 17 | | { |
| | | 18 | | Success, |
| | | 19 | | Unavailable, |
| | | 20 | | BudgetExceeded, |
| | | 21 | | CostOverflow, |
| | | 22 | | CapacityExceeded, |
| | | 23 | | Stale |
| | | 24 | | } |
| | | 25 | | |
| | | 26 | | /// <summary>Certifies one centered volume-body placement through GridForge's prism union.</summary> |
| | | 27 | | internal readonly struct NavigationVolumeAnchorEvaluator |
| | | 28 | | { |
| | | 29 | | private readonly GridWorld _world; |
| | | 30 | | private readonly NavigationWorldGraph _graph; |
| | | 31 | | private readonly NavigationAgentProfile _profile; |
| | | 32 | | private readonly NavigationAreaPolicy _areaPolicy; |
| | | 33 | | private readonly NavigationRayWorkspace _workspace; |
| | | 34 | | |
| | | 35 | | internal NavigationVolumeAnchorEvaluator( |
| | | 36 | | GridWorld world, |
| | | 37 | | NavigationWorldGraph graph, |
| | | 38 | | NavigationAgentProfile profile, |
| | | 39 | | NavigationAreaPolicy areaPolicy, |
| | | 40 | | NavigationRayWorkspace workspace) |
| | | 41 | | { |
| | 3025 | 42 | | _world = world; |
| | 3025 | 43 | | _graph = graph; |
| | 3025 | 44 | | _profile = profile; |
| | 3025 | 45 | | _areaPolicy = areaPolicy; |
| | 3025 | 46 | | _workspace = workspace; |
| | 3025 | 47 | | } |
| | | 48 | | |
| | | 49 | | internal NavigationVolumeAnchorStatus Evaluate( |
| | | 50 | | NavigationNodeRef node, |
| | | 51 | | TraversalMedia requestedMedia, |
| | | 52 | | NavigationWorkMeter meter, |
| | | 53 | | NavigationDependencyWorkspace dependencies, |
| | | 54 | | out Vector3d footAnchor, |
| | | 55 | | out TraversalMedia qualifyingMedia) |
| | | 56 | | { |
| | 431 | 57 | | footAnchor = default; |
| | 431 | 58 | | qualifyingMedia = TraversalMedia.None; |
| | 431 | 59 | | bool hasAddress = _graph.TryGetNodeAddress( |
| | 431 | 60 | | node, |
| | 431 | 61 | | out NavigationCellAddress address); |
| | 431 | 62 | | bool hasState = _graph.TryGetRawNodeState(node, out NavigationNodeState state); |
| | | 63 | | System.Diagnostics.Debug.Assert(hasAddress && hasState, |
| | | 64 | | "Volume anchor evaluation receives a node owned by its immutable graph."); |
| | 431 | 65 | | if (!state.TryGetCenteredVolumeFootAnchor( |
| | 431 | 66 | | _profile.Shape.Height, |
| | 431 | 67 | | out footAnchor)) |
| | | 68 | | { |
| | 1 | 69 | | return NavigationVolumeAnchorStatus.CostOverflow; |
| | | 70 | | } |
| | | 71 | | |
| | 430 | 72 | | return Trace( |
| | 430 | 73 | | address, |
| | 430 | 74 | | address, |
| | 430 | 75 | | footAnchor, |
| | 430 | 76 | | footAnchor, |
| | 430 | 77 | | requestedMedia, |
| | 430 | 78 | | meter, |
| | 430 | 79 | | dependencies, |
| | 430 | 80 | | out qualifyingMedia); |
| | | 81 | | } |
| | | 82 | | |
| | | 83 | | internal NavigationVolumeAnchorStatus EvaluateSegment( |
| | | 84 | | NavigationMediumStateRef source, |
| | | 85 | | NavigationMediumStateRef target, |
| | | 86 | | Vector3d sourceFootAnchor, |
| | | 87 | | Vector3d targetFootAnchor, |
| | | 88 | | NavigationWorkMeter meter, |
| | | 89 | | NavigationDependencyWorkspace dependencies) |
| | | 90 | | { |
| | 1670 | 91 | | bool hasSourceAddress = _graph.TryGetNodeAddress( |
| | 1670 | 92 | | source.Node, |
| | 1670 | 93 | | out NavigationCellAddress sourceAddress); |
| | 1670 | 94 | | bool hasTargetAddress = _graph.TryGetNodeAddress( |
| | 1670 | 95 | | target.Node, |
| | 1670 | 96 | | out NavigationCellAddress targetAddress); |
| | | 97 | | System.Diagnostics.Debug.Assert( |
| | | 98 | | source.Medium == target.Medium && hasSourceAddress && hasTargetAddress); |
| | | 99 | | |
| | 1670 | 100 | | return Trace( |
| | 1670 | 101 | | sourceAddress, |
| | 1670 | 102 | | targetAddress, |
| | 1670 | 103 | | sourceFootAnchor, |
| | 1670 | 104 | | targetFootAnchor, |
| | 1670 | 105 | | NavigationCell.ToMedia(source.Medium), |
| | 1670 | 106 | | meter, |
| | 1670 | 107 | | dependencies, |
| | 1670 | 108 | | out _); |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | private NavigationVolumeAnchorStatus Trace( |
| | | 112 | | NavigationCellAddress sourceAddress, |
| | | 113 | | NavigationCellAddress targetAddress, |
| | | 114 | | Vector3d sourceFootAnchor, |
| | | 115 | | Vector3d targetFootAnchor, |
| | | 116 | | TraversalMedia requestedMedia, |
| | | 117 | | NavigationWorkMeter meter, |
| | | 118 | | NavigationDependencyWorkspace dependencies, |
| | | 119 | | out TraversalMedia qualifyingMedia) |
| | | 120 | | { |
| | 2100 | 121 | | qualifyingMedia = TraversalMedia.None; |
| | 2100 | 122 | | ulong before = _world.ChangeSequence; |
| | 2100 | 123 | | _graph.TryGetMap(sourceAddress.MapId, out NavigationMapInstance? sourceInstance); |
| | 2100 | 124 | | _graph.TryGetMap(targetAddress.MapId, out NavigationMapInstance? targetInstance); |
| | | 125 | | System.Diagnostics.Debug.Assert(sourceInstance != null && targetInstance != null); |
| | | 126 | | |
| | 2100 | 127 | | NavigationGridGenerationIdentity sourceIdentity = sourceInstance!.GridIdentity; |
| | 2100 | 128 | | NavigationGridGenerationIdentity targetIdentity = targetInstance!.GridIdentity; |
| | 2100 | 129 | | var sourceCell = new WorldVoxelIndex( |
| | 2100 | 130 | | sourceIdentity.WorldSpawnToken, |
| | 2100 | 131 | | sourceIdentity.GridIndex, |
| | 2100 | 132 | | sourceIdentity.GridSpawnToken, |
| | 2100 | 133 | | sourceAddress.Index); |
| | 2100 | 134 | | var targetCell = new WorldVoxelIndex( |
| | 2100 | 135 | | targetIdentity.WorldSpawnToken, |
| | 2100 | 136 | | targetIdentity.GridIndex, |
| | 2100 | 137 | | targetIdentity.GridSpawnToken, |
| | 2100 | 138 | | targetAddress.Index); |
| | 2100 | 139 | | int gridLimit = Math.Min(_workspace.MapCapacity, meter.RemainingLookupProbes); |
| | 2100 | 140 | | int addressLimit = Math.Min( |
| | 2100 | 141 | | _workspace.CoveredAddressCapacity, |
| | 2100 | 142 | | meter.RemainingCoveredVoxelIntervals); |
| | 2100 | 143 | | long candidateWorkLimit = meter.RemainingGridCandidateWork; |
| | 2100 | 144 | | meter.RecordVolumeUnionCheck(); |
| | 2100 | 145 | | GridNavigationBodyTraceReport report = GridTracer.TraceNavigationBodyInto( |
| | 2100 | 146 | | _world, |
| | 2100 | 147 | | sourceCell, |
| | 2100 | 148 | | targetCell, |
| | 2100 | 149 | | sourceFootAnchor, |
| | 2100 | 150 | | targetFootAnchor, |
| | 2100 | 151 | | _profile.Shape.Radius, |
| | 2100 | 152 | | _profile.Shape.Height, |
| | 2100 | 153 | | _workspace.BodyTraceCells, |
| | 2100 | 154 | | _workspace.BodyTraceScratch, |
| | 2100 | 155 | | gridLimit, |
| | 2100 | 156 | | addressLimit, |
| | 2100 | 157 | | _workspace.CoveredAddressCapacity, |
| | 2100 | 158 | | candidateWorkLimit); |
| | 2100 | 159 | | bool consumedLookups = meter.TryConsumeLookupProbes(report.GridCandidateCount); |
| | 2100 | 160 | | bool consumedAddresses = |
| | 2100 | 161 | | meter.TryConsumeCoveredVoxelIntervals(report.AddressCandidateCount); |
| | | 162 | | System.Diagnostics.Debug.Assert(consumedLookups && consumedAddresses); |
| | | 163 | | System.Diagnostics.Debug.Assert( |
| | | 164 | | report.Status != GridNavigationBodyTraceStatus.OutputLimitExceeded, |
| | | 165 | | "the address ceiling never exceeds the shared body-trace output capacity"); |
| | 2100 | 166 | | NavigationVolumeAnchorStatus traceStatus = ResolveTraceStatus( |
| | 2100 | 167 | | before, |
| | 2100 | 168 | | _world.ChangeSequence, |
| | 2100 | 169 | | report.Status, |
| | 2100 | 170 | | gridLimit, |
| | 2100 | 171 | | _workspace.MapCapacity, |
| | 2100 | 172 | | addressLimit, |
| | 2100 | 173 | | _workspace.CoveredAddressCapacity); |
| | 2100 | 174 | | if (traceStatus is not NavigationVolumeAnchorStatus.Success |
| | 2100 | 175 | | and not NavigationVolumeAnchorStatus.Unavailable) |
| | | 176 | | { |
| | 27 | 177 | | return traceStatus; |
| | | 178 | | } |
| | | 179 | | |
| | 2073 | 180 | | qualifyingMedia = requestedMedia & TraversalMedia.AnyVolume; |
| | 21234 | 181 | | for (int i = 0; i < _workspace.BodyTraceCells.Count; i++) |
| | | 182 | | { |
| | 8554 | 183 | | GridNavigationBodyTraceCell traceCell = _workspace.BodyTraceCells[i]; |
| | 8554 | 184 | | if (!_graph.TryGetMapId(traceCell.ConfigurationKey, out string mapId)) |
| | 1 | 185 | | return NavigationVolumeAnchorStatus.Stale; |
| | 8553 | 186 | | bool hasTraceInstance = _graph.TryGetMap( |
| | 8553 | 187 | | mapId, |
| | 8553 | 188 | | out NavigationMapInstance traceInstance); |
| | | 189 | | System.Diagnostics.Debug.Assert( |
| | | 190 | | hasTraceInstance, |
| | | 191 | | "the immutable configuration index and map directory are built and replaced together"); |
| | 8553 | 192 | | if (!IsTraceGenerationCurrent( |
| | 8553 | 193 | | traceInstance.GridIdentity.Matches( |
| | 8553 | 194 | | traceCell.Cell.WorldSpawnToken, |
| | 8553 | 195 | | traceCell.Cell.GridIndex, |
| | 8553 | 196 | | traceCell.Cell.GridSpawnToken), |
| | 8553 | 197 | | traceInstance.GridLastChangeSequence, |
| | 8553 | 198 | | traceCell.GridLastChangeSequence)) |
| | | 199 | | { |
| | 3 | 200 | | return NavigationVolumeAnchorStatus.Stale; |
| | | 201 | | } |
| | | 202 | | |
| | 8550 | 203 | | var traceAddress = new NavigationCellAddress(mapId, traceCell.Cell.VoxelIndex); |
| | 8550 | 204 | | if (!_graph.TryGetNodeRef(traceAddress, out NavigationNodeRef traceNode)) |
| | 5 | 205 | | return NavigationVolumeAnchorStatus.Stale; |
| | 8545 | 206 | | if (!dependencies.TryRecordPage( |
| | 8545 | 207 | | mapId, |
| | 8545 | 208 | | traceNode.CellSlot / NavigationSemanticPage.SlotCount)) |
| | | 209 | | { |
| | 1 | 210 | | return NavigationVolumeAnchorStatus.CapacityExceeded; |
| | | 211 | | } |
| | 8544 | 212 | | if (traceCell.Role != GridNavigationBodyTraceCellRole.RequiredCoverage) |
| | | 213 | | continue; |
| | 8543 | 214 | | if (!traceCell.IsPhysicallyPresent) |
| | | 215 | | { |
| | 5 | 216 | | qualifyingMedia = TraversalMedia.None; |
| | 5 | 217 | | continue; |
| | | 218 | | } |
| | | 219 | | |
| | 8538 | 220 | | qualifyingMedia = FilterPassableMedia(traceNode, qualifyingMedia); |
| | | 221 | | } |
| | 2063 | 222 | | NavigationVolumeAnchorStatus result = traceStatus == NavigationVolumeAnchorStatus.Unavailable |
| | 2063 | 223 | | || qualifyingMedia == TraversalMedia.None |
| | 2063 | 224 | | ? NavigationVolumeAnchorStatus.Unavailable |
| | 2063 | 225 | | : NavigationVolumeAnchorStatus.Success; |
| | 2063 | 226 | | return ResolveFinalStatus(before, _world.ChangeSequence, result); |
| | | 227 | | } |
| | | 228 | | |
| | | 229 | | internal static bool IsTraceGenerationCurrent( |
| | | 230 | | bool identityMatches, |
| | | 231 | | ulong instanceLastChangeSequence, |
| | | 232 | | ulong traceLastChangeSequence) => |
| | 8557 | 233 | | identityMatches && instanceLastChangeSequence == traceLastChangeSequence; |
| | | 234 | | |
| | | 235 | | internal static NavigationVolumeAnchorStatus ResolveTraceStatus( |
| | | 236 | | ulong worldSequenceBefore, |
| | | 237 | | ulong worldSequenceAfter, |
| | | 238 | | GridNavigationBodyTraceStatus status, |
| | | 239 | | int gridLimit, |
| | | 240 | | int mapCapacity, |
| | | 241 | | int addressLimit, |
| | | 242 | | int coveredAddressCapacity) |
| | | 243 | | { |
| | 2110 | 244 | | if (worldSequenceBefore != worldSequenceAfter) |
| | 1 | 245 | | return NavigationVolumeAnchorStatus.Stale; |
| | 2109 | 246 | | return status switch |
| | 2109 | 247 | | { |
| | 2109 | 248 | | GridNavigationBodyTraceStatus.Complete => |
| | 2023 | 249 | | NavigationVolumeAnchorStatus.Success, |
| | 2109 | 250 | | GridNavigationBodyTraceStatus.IncompletePhysicalCoverage => |
| | 10 | 251 | | NavigationVolumeAnchorStatus.Unavailable, |
| | 2109 | 252 | | GridNavigationBodyTraceStatus.InvalidOrUnrepresentableGeometry => |
| | 43 | 253 | | NavigationVolumeAnchorStatus.Unavailable, |
| | 2109 | 254 | | GridNavigationBodyTraceStatus.ArithmeticOverflow => |
| | 4 | 255 | | NavigationVolumeAnchorStatus.CostOverflow, |
| | 2109 | 256 | | GridNavigationBodyTraceStatus.GridCandidateLimitExceeded => |
| | 13 | 257 | | gridLimit < mapCapacity |
| | 13 | 258 | | ? NavigationVolumeAnchorStatus.BudgetExceeded |
| | 13 | 259 | | : NavigationVolumeAnchorStatus.CapacityExceeded, |
| | 2109 | 260 | | GridNavigationBodyTraceStatus.AddressLimitExceeded => |
| | 14 | 261 | | addressLimit < coveredAddressCapacity |
| | 14 | 262 | | ? NavigationVolumeAnchorStatus.BudgetExceeded |
| | 14 | 263 | | : NavigationVolumeAnchorStatus.CapacityExceeded, |
| | 2 | 264 | | _ => NavigationVolumeAnchorStatus.BudgetExceeded |
| | 2109 | 265 | | }; |
| | | 266 | | } |
| | | 267 | | |
| | | 268 | | internal static NavigationVolumeAnchorStatus ResolveFinalStatus( |
| | | 269 | | ulong worldSequenceBefore, |
| | | 270 | | ulong worldSequenceAfter, |
| | | 271 | | NavigationVolumeAnchorStatus result) => |
| | 2065 | 272 | | worldSequenceBefore == worldSequenceAfter |
| | 2065 | 273 | | ? result |
| | 2065 | 274 | | : NavigationVolumeAnchorStatus.Stale; |
| | | 275 | | |
| | | 276 | | private TraversalMedia FilterPassableMedia( |
| | | 277 | | NavigationNodeRef node, |
| | | 278 | | TraversalMedia media) |
| | | 279 | | { |
| | 8538 | 280 | | if ((media & TraversalMedia.Gas) != 0 |
| | 8538 | 281 | | && !new TraversalEvaluator( |
| | 8538 | 282 | | _graph, |
| | 8538 | 283 | | _profile, |
| | 8538 | 284 | | _areaPolicy, |
| | 8538 | 285 | | TraversalMedium.Gas) |
| | 8538 | 286 | | .TryGetPassableNodeState(node, out _)) |
| | | 287 | | { |
| | 6 | 288 | | media &= ~TraversalMedia.Gas; |
| | | 289 | | } |
| | 8538 | 290 | | if ((media & TraversalMedia.Liquid) != 0 |
| | 8538 | 291 | | && !new TraversalEvaluator( |
| | 8538 | 292 | | _graph, |
| | 8538 | 293 | | _profile, |
| | 8538 | 294 | | _areaPolicy, |
| | 8538 | 295 | | TraversalMedium.Liquid) |
| | 8538 | 296 | | .TryGetPassableNodeState(node, out _)) |
| | | 297 | | { |
| | 5 | 298 | | media &= ~TraversalMedia.Liquid; |
| | | 299 | | } |
| | 8538 | 300 | | return media; |
| | | 301 | | } |
| | | 302 | | |
| | | 303 | | } |