| | | 1 | | //======================================================================= |
| | | 2 | | // GridDiagnosticSession.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 | | using GridForge.Grids.Topology; |
| | | 11 | | using GridForge.Spatial; |
| | | 12 | | using SwiftCollections; |
| | | 13 | | |
| | | 14 | | namespace GridForge.Diagnostics; |
| | | 15 | | |
| | | 16 | | /// <summary> |
| | | 17 | | /// Captures dirty diagnostic grid, cell, and sparse address changes for one |
| | | 18 | | /// active <see cref="GridWorld"/>. |
| | | 19 | | /// </summary> |
| | | 20 | | public sealed class GridDiagnosticSession : IDisposable |
| | | 21 | | { |
| | | 22 | | private readonly GridWorld _world; |
| | | 23 | | private readonly long _worldSpawnToken; |
| | 16 | 24 | | private readonly SwiftList<GridDiagnosticChange> _changes = new(); |
| | 16 | 25 | | private readonly SwiftDictionary<GridDiagnosticChange, int> _changeIndexes = new(); |
| | 16 | 26 | | private readonly object _syncRoot = new(); |
| | | 27 | | private bool _worldResetPending; |
| | | 28 | | private bool _disposed; |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Creates a diagnostic dirty-tracking session for the supplied active |
| | | 32 | | /// world. |
| | | 33 | | /// </summary> |
| | 16 | 34 | | public GridDiagnosticSession(GridWorld world) |
| | | 35 | | { |
| | 16 | 36 | | if (world == null) |
| | 1 | 37 | | throw new ArgumentNullException(nameof(world)); |
| | | 38 | | |
| | 15 | 39 | | if (!world.IsActive) |
| | 1 | 40 | | throw new InvalidOperationException("Diagnostic sessions require an active GridWorld."); |
| | | 41 | | |
| | 14 | 42 | | _world = world; |
| | 14 | 43 | | _worldSpawnToken = world.SpawnToken; |
| | 14 | 44 | | Subscribe(); |
| | 14 | 45 | | } |
| | | 46 | | |
| | | 47 | | /// <summary> |
| | | 48 | | /// Clears and fills caller-owned storage with coalesced dirty changes. |
| | | 49 | | /// </summary> |
| | | 50 | | public int GetDirtyChangesInto(SwiftList<GridDiagnosticChange> results) |
| | | 51 | | { |
| | 17 | 52 | | SwiftThrowHelper.ThrowIfNull(results, nameof(results)); |
| | | 53 | | |
| | 17 | 54 | | results.Clear(); |
| | 17 | 55 | | lock (_syncRoot) |
| | | 56 | | { |
| | 64 | 57 | | for (int i = 0; i < _changes.Count; i++) |
| | 15 | 58 | | results.Add(_changes[i]); |
| | 17 | 59 | | } |
| | | 60 | | |
| | 17 | 61 | | if (results.Count > 1) |
| | 5 | 62 | | results.SortInPlace(); |
| | | 63 | | |
| | 17 | 64 | | return results.Count; |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | /// <summary> |
| | | 68 | | /// Clears all dirty changes captured so far. |
| | | 69 | | /// </summary> |
| | | 70 | | public void ClearDirtyChanges() |
| | | 71 | | { |
| | 17 | 72 | | lock (_syncRoot) |
| | | 73 | | { |
| | 17 | 74 | | _changes.Clear(); |
| | 17 | 75 | | _changeIndexes.Clear(); |
| | 17 | 76 | | _worldResetPending = false; |
| | 17 | 77 | | } |
| | 17 | 78 | | } |
| | | 79 | | |
| | | 80 | | /// <inheritdoc/> |
| | | 81 | | public void Dispose() |
| | | 82 | | { |
| | 17 | 83 | | if (_disposed) |
| | 3 | 84 | | return; |
| | | 85 | | |
| | 14 | 86 | | Unsubscribe(); |
| | 14 | 87 | | ClearDirtyChanges(); |
| | 14 | 88 | | _disposed = true; |
| | 14 | 89 | | GC.SuppressFinalize(this); |
| | 14 | 90 | | } |
| | | 91 | | |
| | | 92 | | private void Subscribe() |
| | | 93 | | { |
| | 14 | 94 | | _world.OnActiveGridAdded += HandleActiveGridAdded; |
| | 14 | 95 | | _world.OnActiveGridRemoved += HandleActiveGridRemoved; |
| | 14 | 96 | | _world.OnActiveGridChange += HandleActiveGridChanged; |
| | 14 | 97 | | _world.OnReset += HandleWorldReset; |
| | 14 | 98 | | GridObstacleManager.OnObstacleAdded += HandleObstacleChanged; |
| | 14 | 99 | | GridObstacleManager.OnObstacleRemoved += HandleObstacleChanged; |
| | 14 | 100 | | GridObstacleManager.OnObstaclesCleared += HandleObstaclesCleared; |
| | 14 | 101 | | GridOccupantManager.OnOccupantAdded += HandleOccupantChanged; |
| | 14 | 102 | | GridOccupantManager.OnOccupantRemoved += HandleOccupantChanged; |
| | 14 | 103 | | } |
| | | 104 | | |
| | | 105 | | private void Unsubscribe() |
| | | 106 | | { |
| | 14 | 107 | | _world.OnActiveGridAdded -= HandleActiveGridAdded; |
| | 14 | 108 | | _world.OnActiveGridRemoved -= HandleActiveGridRemoved; |
| | 14 | 109 | | _world.OnActiveGridChange -= HandleActiveGridChanged; |
| | 14 | 110 | | _world.OnReset -= HandleWorldReset; |
| | 14 | 111 | | GridObstacleManager.OnObstacleAdded -= HandleObstacleChanged; |
| | 14 | 112 | | GridObstacleManager.OnObstacleRemoved -= HandleObstacleChanged; |
| | 14 | 113 | | GridObstacleManager.OnObstaclesCleared -= HandleObstaclesCleared; |
| | 14 | 114 | | GridOccupantManager.OnOccupantAdded -= HandleOccupantChanged; |
| | 14 | 115 | | GridOccupantManager.OnOccupantRemoved -= HandleOccupantChanged; |
| | 14 | 116 | | } |
| | | 117 | | |
| | | 118 | | private void HandleActiveGridAdded(GridEventInfo eventInfo) |
| | | 119 | | { |
| | 4 | 120 | | if (!CanRecordGrid(eventInfo, out _)) |
| | 1 | 121 | | return; |
| | | 122 | | |
| | 3 | 123 | | RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridAdded); |
| | 3 | 124 | | } |
| | | 125 | | |
| | | 126 | | private void HandleActiveGridRemoved(GridEventInfo eventInfo) |
| | | 127 | | { |
| | 3 | 128 | | if (!CanRecord(eventInfo.WorldSpawnToken)) |
| | 1 | 129 | | return; |
| | | 130 | | |
| | 2 | 131 | | RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridRemoved); |
| | 2 | 132 | | } |
| | | 133 | | |
| | | 134 | | private void HandleActiveGridChanged(GridEventInfo eventInfo) |
| | | 135 | | { |
| | 10 | 136 | | if (!CanRecordGrid(eventInfo, out VoxelGrid? grid) || HasPendingWorldReset()) |
| | 3 | 137 | | return; |
| | | 138 | | |
| | 7 | 139 | | switch (eventInfo.ChangeKind) |
| | | 140 | | { |
| | | 141 | | case GridEventKind.SparseVoxelAdded: |
| | 1 | 142 | | RecordSparseVoxelChange(eventInfo, grid!, GridDiagnosticChangeKind.SparseVoxelAdded); |
| | 1 | 143 | | break; |
| | | 144 | | case GridEventKind.SparseVoxelRemoved: |
| | 1 | 145 | | RecordSparseVoxelChange(eventInfo, grid!, GridDiagnosticChangeKind.SparseVoxelRemoved); |
| | 1 | 146 | | break; |
| | | 147 | | default: |
| | 5 | 148 | | RecordGridChange(eventInfo, GridDiagnosticChangeKind.GridChanged); |
| | | 149 | | break; |
| | | 150 | | } |
| | 5 | 151 | | } |
| | | 152 | | |
| | | 153 | | private void HandleWorldReset() |
| | | 154 | | { |
| | 3 | 155 | | if (!CanRecord(_worldSpawnToken)) |
| | 1 | 156 | | return; |
| | | 157 | | |
| | 2 | 158 | | GridDiagnosticChange change = new( |
| | 2 | 159 | | GridDiagnosticChangeKind.WorldReset, |
| | 2 | 160 | | _worldSpawnToken, |
| | 2 | 161 | | ushort.MaxValue, |
| | 2 | 162 | | 0, |
| | 2 | 163 | | default, |
| | 2 | 164 | | default, |
| | 2 | 165 | | default, |
| | 2 | 166 | | default); |
| | 2 | 167 | | GridDiagnosticChange key = CreateChangeKey(change); |
| | | 168 | | |
| | 2 | 169 | | lock (_syncRoot) |
| | | 170 | | { |
| | 2 | 171 | | _changes.Clear(); |
| | 2 | 172 | | _changeIndexes.Clear(); |
| | 2 | 173 | | _worldResetPending = true; |
| | 2 | 174 | | _changeIndexes.Add(key, 0); |
| | 2 | 175 | | _changes.Add(change); |
| | 2 | 176 | | } |
| | 2 | 177 | | } |
| | | 178 | | |
| | | 179 | | private void HandleObstacleChanged(ObstacleEventInfo eventInfo) |
| | | 180 | | { |
| | 7 | 181 | | if (!CanRecordCell(eventInfo.VoxelIndex)) |
| | 3 | 182 | | return; |
| | | 183 | | |
| | 4 | 184 | | RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged); |
| | 4 | 185 | | } |
| | | 186 | | |
| | | 187 | | private void HandleObstaclesCleared(ObstacleClearEventInfo eventInfo) |
| | | 188 | | { |
| | 2 | 189 | | if (!CanRecordCell(eventInfo.VoxelIndex)) |
| | 1 | 190 | | return; |
| | | 191 | | |
| | 1 | 192 | | RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.ObstacleChanged); |
| | 1 | 193 | | } |
| | | 194 | | |
| | | 195 | | private void HandleOccupantChanged(OccupantEventInfo eventInfo) |
| | | 196 | | { |
| | 4 | 197 | | if (!CanRecordCell(eventInfo.VoxelIndex)) |
| | 1 | 198 | | return; |
| | | 199 | | |
| | 3 | 200 | | RecordCellChange(eventInfo.VoxelIndex, GridDiagnosticChangeKind.OccupantChanged); |
| | 3 | 201 | | } |
| | | 202 | | |
| | | 203 | | private bool CanRecord(long worldSpawnToken) => |
| | 33 | 204 | | !_disposed |
| | 33 | 205 | | && worldSpawnToken == _worldSpawnToken; |
| | | 206 | | |
| | | 207 | | private bool CanRecordGrid(GridEventInfo eventInfo, out VoxelGrid? grid) |
| | | 208 | | { |
| | 14 | 209 | | grid = null; |
| | 14 | 210 | | return CanRecord(eventInfo.WorldSpawnToken) |
| | 14 | 211 | | && _world.TryGetGrid(eventInfo.GridIndex, out grid) |
| | 14 | 212 | | && grid!.SpawnToken == eventInfo.GridSpawnToken; |
| | | 213 | | } |
| | | 214 | | |
| | | 215 | | private bool CanRecordCell(WorldVoxelIndex worldIndex) => |
| | 13 | 216 | | CanRecord(worldIndex.WorldSpawnToken) |
| | 13 | 217 | | && !HasPendingWorldReset() |
| | 13 | 218 | | && _world.TryGetGrid(worldIndex, out _); |
| | | 219 | | |
| | | 220 | | private bool HasPendingWorldReset() |
| | | 221 | | { |
| | 17 | 222 | | lock (_syncRoot) |
| | 17 | 223 | | return _worldResetPending; |
| | 17 | 224 | | } |
| | | 225 | | |
| | | 226 | | private void RecordGridChange( |
| | | 227 | | GridEventInfo eventInfo, |
| | | 228 | | GridDiagnosticChangeKind kind) |
| | | 229 | | { |
| | 10 | 230 | | RecordChange(new GridDiagnosticChange( |
| | 10 | 231 | | kind, |
| | 10 | 232 | | eventInfo.WorldSpawnToken, |
| | 10 | 233 | | eventInfo.GridIndex, |
| | 10 | 234 | | eventInfo.GridSpawnToken, |
| | 10 | 235 | | default, |
| | 10 | 236 | | default, |
| | 10 | 237 | | eventInfo.BoundsMin, |
| | 10 | 238 | | eventInfo.BoundsMax)); |
| | 10 | 239 | | } |
| | | 240 | | |
| | | 241 | | private void RecordSparseVoxelChange( |
| | | 242 | | GridEventInfo eventInfo, |
| | | 243 | | VoxelGrid grid, |
| | | 244 | | GridDiagnosticChangeKind kind) |
| | | 245 | | { |
| | 2 | 246 | | WorldVoxelIndex worldIndex = new( |
| | 2 | 247 | | eventInfo.WorldSpawnToken, |
| | 2 | 248 | | eventInfo.GridIndex, |
| | 2 | 249 | | eventInfo.GridSpawnToken, |
| | 2 | 250 | | eventInfo.VoxelIndex); |
| | | 251 | | |
| | 2 | 252 | | RecordChange(new GridDiagnosticChange( |
| | 2 | 253 | | kind, |
| | 2 | 254 | | eventInfo.WorldSpawnToken, |
| | 2 | 255 | | eventInfo.GridIndex, |
| | 2 | 256 | | eventInfo.GridSpawnToken, |
| | 2 | 257 | | worldIndex, |
| | 2 | 258 | | eventInfo.VoxelIndex, |
| | 2 | 259 | | eventInfo.AffectedBoundsMin, |
| | 2 | 260 | | eventInfo.AffectedBoundsMax)); |
| | | 261 | | |
| | 2 | 262 | | RecordSparseAddressRangeChange(eventInfo, grid); |
| | 2 | 263 | | } |
| | | 264 | | |
| | | 265 | | private void RecordSparseAddressRangeChange(GridEventInfo eventInfo, VoxelGrid grid) |
| | | 266 | | { |
| | 2 | 267 | | TopologyVoxelAabb bounds = TopologyVoxelAabb.FromIndex(grid, eventInfo.VoxelIndex); |
| | | 268 | | |
| | 2 | 269 | | RecordChange(new GridDiagnosticChange( |
| | 2 | 270 | | GridDiagnosticChangeKind.SparseAddressChanged, |
| | 2 | 271 | | eventInfo.WorldSpawnToken, |
| | 2 | 272 | | eventInfo.GridIndex, |
| | 2 | 273 | | eventInfo.GridSpawnToken, |
| | 2 | 274 | | default, |
| | 2 | 275 | | eventInfo.VoxelIndex, |
| | 2 | 276 | | bounds.Min, |
| | 2 | 277 | | bounds.Max)); |
| | 2 | 278 | | } |
| | | 279 | | |
| | | 280 | | private void RecordCellChange( |
| | | 281 | | WorldVoxelIndex worldIndex, |
| | | 282 | | GridDiagnosticChangeKind kind) |
| | | 283 | | { |
| | 8 | 284 | | RecordChange(new GridDiagnosticChange( |
| | 8 | 285 | | kind, |
| | 8 | 286 | | worldIndex.WorldSpawnToken, |
| | 8 | 287 | | worldIndex.GridIndex, |
| | 8 | 288 | | worldIndex.GridSpawnToken, |
| | 8 | 289 | | worldIndex, |
| | 8 | 290 | | worldIndex.VoxelIndex, |
| | 8 | 291 | | default, |
| | 8 | 292 | | default)); |
| | 8 | 293 | | } |
| | | 294 | | |
| | | 295 | | private void RecordChange(GridDiagnosticChange change) |
| | | 296 | | { |
| | 22 | 297 | | GridDiagnosticChange key = CreateChangeKey(change); |
| | 22 | 298 | | lock (_syncRoot) |
| | | 299 | | { |
| | 22 | 300 | | if (_changeIndexes.TryGetValue(key, out int index)) |
| | | 301 | | { |
| | 6 | 302 | | GridDiagnosticChange existing = _changes[index]; |
| | 6 | 303 | | _changes[index] = existing.WithKind(existing.Kind | change.Kind); |
| | 6 | 304 | | return; |
| | | 305 | | } |
| | | 306 | | |
| | 16 | 307 | | _changeIndexes.Add(key, _changes.Count); |
| | 16 | 308 | | _changes.Add(change); |
| | 16 | 309 | | } |
| | 22 | 310 | | } |
| | | 311 | | |
| | | 312 | | private static GridDiagnosticChange CreateChangeKey(GridDiagnosticChange change) => |
| | 24 | 313 | | change.WithKind((GridDiagnosticChangeKind)GetScope(change)); |
| | | 314 | | |
| | | 315 | | private static GridDiagnosticChangeScope GetScope(GridDiagnosticChange change) |
| | | 316 | | { |
| | 24 | 317 | | if ((change.Kind & GridDiagnosticChangeKind.WorldReset) != 0) |
| | 2 | 318 | | return GridDiagnosticChangeScope.World; |
| | | 319 | | |
| | 22 | 320 | | if (change.WorldIndex.WorldSpawnToken != 0 || change.WorldIndex.VoxelIndex.IsAllocated) |
| | 10 | 321 | | return GridDiagnosticChangeScope.Cell; |
| | | 322 | | |
| | 12 | 323 | | return (change.Kind & GridDiagnosticChangeKind.SparseAddressChanged) != 0 |
| | 12 | 324 | | ? GridDiagnosticChangeScope.Range |
| | 12 | 325 | | : GridDiagnosticChangeScope.Grid; |
| | | 326 | | } |
| | | 327 | | |
| | | 328 | | private enum GridDiagnosticChangeScope |
| | | 329 | | { |
| | | 330 | | World = 0, |
| | | 331 | | Grid = 1, |
| | | 332 | | Cell = 2, |
| | | 333 | | Range = 3 |
| | | 334 | | } |
| | | 335 | | } |