| | | 1 | | //======================================================================= |
| | | 2 | | // NavigationQueryAdmissionWork.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 GridForge.Grids; |
| | | 10 | | |
| | | 11 | | namespace Trailblazer.Pathing; |
| | | 12 | | |
| | | 13 | | /// <summary>Reports bounded internal query-admission progress.</summary> |
| | | 14 | | internal enum NavigationQueryAdmissionStatus : byte |
| | | 15 | | { |
| | | 16 | | Pending = 0, |
| | | 17 | | Success = 1, |
| | | 18 | | Unsupported = 2, |
| | | 19 | | NoMap = 3, |
| | | 20 | | InvalidProfile = 4, |
| | | 21 | | InvalidStart = 5, |
| | | 22 | | InvalidEnd = 6, |
| | | 23 | | NoPath = 7, |
| | | 24 | | BudgetExceeded = 8, |
| | | 25 | | CostOverflow = 9, |
| | | 26 | | CapacityExceeded = 10, |
| | | 27 | | Stale = 11 |
| | | 28 | | } |
| | | 29 | | |
| | | 30 | | /// <summary>Resolves one surface A* query against an exact leased graph generation.</summary> |
| | | 31 | | internal sealed class NavigationQueryAdmissionWork : IDisposable |
| | | 32 | | { |
| | | 33 | | private enum Stage : byte |
| | | 34 | | { |
| | | 35 | | ResolvePolicy = 0, |
| | | 36 | | ResolveStart = 1, |
| | | 37 | | ResolveEnd = 2 |
| | | 38 | | } |
| | | 39 | | |
| | | 40 | | private readonly NavigationEndpointWorkspace _workspace; |
| | | 41 | | private readonly GridWorld _world; |
| | | 42 | | private readonly PathAlgorithm _expectedAlgorithm; |
| | | 43 | | private readonly NavigationWorkMeter _meter; |
| | | 44 | | private readonly NavigationRayWork _rayWork; |
| | | 45 | | private readonly NavigationEndpointResolutionWork _endpointWork; |
| | | 46 | | private readonly NavigationResolvedPathQuery _result; |
| | | 47 | | private NavigationWorldGraphLease? _lease; |
| | | 48 | | private NavigationAreaPolicy? _areaPolicy; |
| | | 49 | | private PathQuery _query; |
| | | 50 | | private TraversalMedium _startMedium; |
| | | 51 | | private TraversalMedia _targetMedia; |
| | | 52 | | private NavigationResolvedEndpoint _start; |
| | | 53 | | private NavigationResolvedEndpoint _end; |
| | | 54 | | private Stage _stage; |
| | | 55 | | private bool _endpointActive; |
| | | 56 | | private bool _active; |
| | | 57 | | private ulong _worldChangeSequence; |
| | | 58 | | private bool _requiresWorldStamp; |
| | | 59 | | |
| | 15816 | 60 | | internal NavigationQueryAdmissionWork( |
| | 15816 | 61 | | GridWorld world, |
| | 15816 | 62 | | NavigationWorldGraphStore store, |
| | 15816 | 63 | | NavigationEndpointWorkspace workspace, |
| | 15816 | 64 | | NavigationRayWorkspace rayWorkspace, |
| | 15816 | 65 | | PathAlgorithm expectedAlgorithm) |
| | | 66 | | { |
| | 15816 | 67 | | SwiftThrowHelper.ThrowIfNull(world, nameof(world)); |
| | 15816 | 68 | | SwiftThrowHelper.ThrowIfNull(store, nameof(store)); |
| | 15816 | 69 | | SwiftThrowHelper.ThrowIfNull(workspace, nameof(workspace)); |
| | 15816 | 70 | | SwiftThrowHelper.ThrowIfNull(rayWorkspace, nameof(rayWorkspace)); |
| | 15816 | 71 | | _workspace = workspace; |
| | 15816 | 72 | | _world = world; |
| | 15816 | 73 | | _expectedAlgorithm = expectedAlgorithm; |
| | 15816 | 74 | | _meter = new NavigationWorkMeter(default); |
| | 15816 | 75 | | _rayWork = new NavigationRayWork(rayWorkspace); |
| | 15816 | 76 | | _endpointWork = new NavigationEndpointResolutionWork( |
| | 15816 | 77 | | world, |
| | 15816 | 78 | | store, |
| | 15816 | 79 | | _meter, |
| | 15816 | 80 | | workspace, |
| | 15816 | 81 | | rayWorkspace, |
| | 15816 | 82 | | _rayWork); |
| | 15816 | 83 | | _result = new NavigationResolvedPathQuery(); |
| | 15816 | 84 | | } |
| | | 85 | | |
| | | 86 | | internal void Begin( |
| | | 87 | | NavigationWorldGraphLease lease, |
| | | 88 | | PathQuery query, |
| | | 89 | | TraversalMedium startMedium, |
| | | 90 | | TraversalMedia targetMedia) |
| | | 91 | | { |
| | 689 | 92 | | SwiftThrowHelper.ThrowIfNull(lease, nameof(lease)); |
| | 689 | 93 | | SwiftThrowHelper.ThrowIfTrue( |
| | 689 | 94 | | _active, |
| | 689 | 95 | | message: "The query admission work is already active."); |
| | 688 | 96 | | _active = true; |
| | 688 | 97 | | _lease = lease; |
| | 688 | 98 | | _query = query; |
| | 688 | 99 | | _workspace.Reset(); |
| | 688 | 100 | | _meter.Reset(query.Budget); |
| | 688 | 101 | | _areaPolicy = null; |
| | 688 | 102 | | _start = default; |
| | 688 | 103 | | _end = default; |
| | 688 | 104 | | _worldChangeSequence = _world.ChangeSequence; |
| | 688 | 105 | | _startMedium = startMedium; |
| | 688 | 106 | | _targetMedia = targetMedia; |
| | 688 | 107 | | _stage = Stage.ResolvePolicy; |
| | 688 | 108 | | _endpointActive = false; |
| | 688 | 109 | | _requiresWorldStamp = false; |
| | 688 | 110 | | Status = NavigationQueryAdmissionStatus.Pending; |
| | 688 | 111 | | if (!query.Agent.IsValid) |
| | 2 | 112 | | Finish(NavigationQueryAdmissionStatus.InvalidProfile); |
| | 686 | 113 | | else if (query.Algorithm != _expectedAlgorithm) |
| | 4 | 114 | | Finish(NavigationQueryAdmissionStatus.Unsupported); |
| | 682 | 115 | | else if (!NavigationCell.IsKnownMedium(startMedium)) |
| | 1 | 116 | | Finish(NavigationQueryAdmissionStatus.InvalidStart); |
| | 681 | 117 | | else if (targetMedia == TraversalMedia.None |
| | 681 | 118 | | || (targetMedia & ~NavigationCell.KnownMedia) != 0) |
| | | 119 | | { |
| | 2 | 120 | | Finish(NavigationQueryAdmissionStatus.InvalidEnd); |
| | | 121 | | } |
| | 679 | 122 | | else if ((query.Agent.AllowedMedia & NavigationCell.ToMedia(startMedium)) == 0 |
| | 679 | 123 | | || (targetMedia & ~query.Agent.AllowedMedia) != 0) |
| | | 124 | | { |
| | 2 | 125 | | Finish(NavigationQueryAdmissionStatus.InvalidProfile); |
| | | 126 | | } |
| | 677 | 127 | | else if (!query.AllowTransitions |
| | 677 | 128 | | && (targetMedia & NavigationCell.ToMedia(startMedium)) == 0) |
| | | 129 | | { |
| | 2 | 130 | | Finish(NavigationQueryAdmissionStatus.NoPath); |
| | | 131 | | } |
| | 677 | 132 | | } |
| | | 133 | | |
| | | 134 | | internal NavigationQueryAdmissionStatus Status { get; private set; } |
| | | 135 | | |
| | 721 | 136 | | internal NavigationResolvedPathQuery Result => _result; |
| | | 137 | | |
| | 161 | 138 | | internal NavigationWorkMeter Meter => _meter; |
| | | 139 | | |
| | 250 | 140 | | internal NavigationRayWork RayWork => _rayWork; |
| | | 141 | | |
| | | 142 | | internal NavigationQueryAdmissionStatus Advance( |
| | | 143 | | int lookupStepLimit, |
| | | 144 | | int endpointCandidateStepLimit) |
| | | 145 | | { |
| | 1112 | 146 | | SwiftThrowHelper.ThrowIfNegative(lookupStepLimit, nameof(lookupStepLimit)); |
| | 1112 | 147 | | SwiftThrowHelper.ThrowIfNegative( |
| | 1112 | 148 | | endpointCandidateStepLimit, |
| | 1112 | 149 | | nameof(endpointCandidateStepLimit)); |
| | 1112 | 150 | | if (Status != NavigationQueryAdmissionStatus.Pending) |
| | 6 | 151 | | return Status; |
| | | 152 | | |
| | 1106 | 153 | | int lookupRemaining = lookupStepLimit; |
| | 1106 | 154 | | int candidateRemaining = endpointCandidateStepLimit; |
| | 3021 | 155 | | while (Status == NavigationQueryAdmissionStatus.Pending) |
| | | 156 | | { |
| | 2390 | 157 | | if (_stage == Stage.ResolvePolicy) |
| | | 158 | | { |
| | 651 | 159 | | if (lookupRemaining == 0) |
| | | 160 | | { |
| | 4 | 161 | | return _meter.RemainingLookupProbes == 0 |
| | 4 | 162 | | ? Finish(NavigationQueryAdmissionStatus.BudgetExceeded) |
| | 4 | 163 | | : Status; |
| | | 164 | | } |
| | 647 | 165 | | if (!_meter.TryConsumeLookupProbes(1)) |
| | 1 | 166 | | return Finish(NavigationQueryAdmissionStatus.BudgetExceeded); |
| | 646 | 167 | | lookupRemaining--; |
| | 646 | 168 | | if (!_lease!.Graph.AreaCatalog.TryGet( |
| | 646 | 169 | | _query.AreaPolicy, |
| | 646 | 170 | | out NavigationAreaPolicy policy)) |
| | | 171 | | { |
| | 1 | 172 | | return Finish(NavigationQueryAdmissionStatus.Stale); |
| | | 173 | | } |
| | 645 | 174 | | _areaPolicy = policy; |
| | 645 | 175 | | _stage = Stage.ResolveStart; |
| | 645 | 176 | | continue; |
| | | 177 | | } |
| | | 178 | | |
| | | 179 | | // ResolvePolicy always continues after selecting ResolveStart. Those are the only |
| | | 180 | | // remaining stages while status is pending, so endpoint work is unconditional here. |
| | 1739 | 181 | | if (!_endpointActive) |
| | | 182 | | { |
| | 1284 | 183 | | BeginEndpointWork( |
| | 1284 | 184 | | _stage == Stage.ResolveStart ? _query.Start : _query.End, |
| | 1284 | 185 | | _stage == Stage.ResolveStart |
| | 1284 | 186 | | ? NavigationEndpointRole.Start |
| | 1284 | 187 | | : NavigationEndpointRole.Destination); |
| | 1284 | 188 | | _endpointActive = true; |
| | | 189 | | } |
| | 1739 | 190 | | int lookupBefore = _meter.LookupProbes; |
| | 1739 | 191 | | int candidatesBefore = _meter.EndpointCandidates; |
| | 1739 | 192 | | NavigationEndpointResolutionStatus endpointStatus = _endpointWork.Advance( |
| | 1739 | 193 | | lookupRemaining, |
| | 1739 | 194 | | candidateRemaining); |
| | 1739 | 195 | | lookupRemaining = Math.Max( |
| | 1739 | 196 | | 0, |
| | 1739 | 197 | | lookupRemaining - (_meter.LookupProbes - lookupBefore)); |
| | 1739 | 198 | | candidateRemaining = Math.Max( |
| | 1739 | 199 | | 0, |
| | 1739 | 200 | | candidateRemaining - (_meter.EndpointCandidates - candidatesBefore)); |
| | 1739 | 201 | | if (endpointStatus == NavigationEndpointResolutionStatus.Pending) |
| | 455 | 202 | | return Status; |
| | 1284 | 203 | | if (endpointStatus != NavigationEndpointResolutionStatus.Success) |
| | | 204 | | { |
| | 13 | 205 | | return Finish(MapEndpointFailure(endpointStatus, _stage)); |
| | | 206 | | } |
| | 1271 | 207 | | _requiresWorldStamp = _requiresWorldStamp |
| | 1271 | 208 | | || _endpointWork.RequiresWorldStamp; |
| | 1271 | 209 | | if (_stage == Stage.ResolveStart) |
| | | 210 | | { |
| | 639 | 211 | | _start = _endpointWork.Result; |
| | 639 | 212 | | _stage = Stage.ResolveEnd; |
| | 639 | 213 | | _endpointActive = false; |
| | | 214 | | } |
| | | 215 | | else |
| | | 216 | | { |
| | 632 | 217 | | if (_world.ChangeSequence != _worldChangeSequence) |
| | 1 | 218 | | return Finish(NavigationQueryAdmissionStatus.Stale); |
| | 631 | 219 | | _end = _endpointWork.Result; |
| | 631 | 220 | | _endpointWork.Reset(); |
| | 631 | 221 | | NavigationWorldGraphLease lease = _lease!; |
| | 631 | 222 | | _result.Bind( |
| | 631 | 223 | | lease, |
| | 631 | 224 | | _query, |
| | 631 | 225 | | _start, |
| | 631 | 226 | | _end, |
| | 631 | 227 | | _areaPolicy!, |
| | 631 | 228 | | _startMedium, |
| | 631 | 229 | | _targetMedia, |
| | 631 | 230 | | _meter, |
| | 631 | 231 | | _worldChangeSequence, |
| | 631 | 232 | | _requiresWorldStamp); |
| | 631 | 233 | | _lease = null; |
| | 631 | 234 | | Status = NavigationQueryAdmissionStatus.Success; |
| | 631 | 235 | | _endpointActive = false; |
| | | 236 | | } |
| | | 237 | | } |
| | 631 | 238 | | return Status; |
| | | 239 | | } |
| | | 240 | | |
| | | 241 | | public void Dispose() |
| | | 242 | | { |
| | 720 | 243 | | NavigationWorldGraphLease? lease = _lease; |
| | 720 | 244 | | _lease = null; |
| | 720 | 245 | | lease?.Dispose(); |
| | 720 | 246 | | _endpointWork.Reset(); |
| | 720 | 247 | | _areaPolicy = null; |
| | 720 | 248 | | _query = default; |
| | 720 | 249 | | _startMedium = default; |
| | 720 | 250 | | _targetMedia = default; |
| | 720 | 251 | | _start = default; |
| | 720 | 252 | | _end = default; |
| | 720 | 253 | | _worldChangeSequence = 0; |
| | 720 | 254 | | _requiresWorldStamp = false; |
| | 720 | 255 | | _endpointActive = false; |
| | 720 | 256 | | _active = false; |
| | 720 | 257 | | } |
| | | 258 | | |
| | | 259 | | private void BeginEndpointWork( |
| | | 260 | | NavigationEndpoint endpoint, |
| | | 261 | | NavigationEndpointRole role) |
| | | 262 | | { |
| | 1284 | 263 | | NavigationWorldGraph graph = _lease!.Graph; |
| | 1284 | 264 | | TraversalMedia effectiveTargetMedia = _query.AllowTransitions |
| | 1284 | 265 | | ? _targetMedia |
| | 1284 | 266 | | : NavigationCell.ToMedia(_startMedium); |
| | 1284 | 267 | | _endpointWork.Begin( |
| | 1284 | 268 | | graph, |
| | 1284 | 269 | | endpoint, |
| | 1284 | 270 | | role, |
| | 1284 | 271 | | _query.Agent, |
| | 1284 | 272 | | _areaPolicy!, |
| | 1284 | 273 | | role == NavigationEndpointRole.Start |
| | 1284 | 274 | | ? NavigationCell.ToMedia(_startMedium) |
| | 1284 | 275 | | : effectiveTargetMedia); |
| | 1284 | 276 | | } |
| | | 277 | | |
| | | 278 | | private NavigationQueryAdmissionStatus Finish( |
| | | 279 | | NavigationQueryAdmissionStatus status) |
| | | 280 | | { |
| | 30 | 281 | | Status = status; |
| | 30 | 282 | | Dispose(); |
| | 30 | 283 | | return Status; |
| | | 284 | | } |
| | | 285 | | |
| | | 286 | | private static NavigationQueryAdmissionStatus MapEndpointFailure( |
| | | 287 | | NavigationEndpointResolutionStatus endpointStatus, |
| | | 288 | | Stage stage) |
| | | 289 | | { |
| | 13 | 290 | | if (endpointStatus == NavigationEndpointResolutionStatus.InvalidEndpoint) |
| | | 291 | | { |
| | 8 | 292 | | return stage == Stage.ResolveStart |
| | 8 | 293 | | ? NavigationQueryAdmissionStatus.InvalidStart |
| | 8 | 294 | | : NavigationQueryAdmissionStatus.InvalidEnd; |
| | | 295 | | } |
| | 5 | 296 | | int offset = endpointStatus == NavigationEndpointResolutionStatus.NoMap ? 1 : 4; |
| | 5 | 297 | | return (NavigationQueryAdmissionStatus)((byte)endpointStatus + offset); |
| | | 298 | | } |
| | | 299 | | |
| | | 300 | | } |