< Summary

Information
Class: GridForge.Grids.ScanCell
Assembly: GridForge
File(s): /home/runner/work/GridForge/GridForge/src/GridForge/Grids/Nodes/ScanCell.cs
Line coverage
100%
Covered lines: 135
Uncovered lines: 0
Coverable lines: 135
Total lines: 448
Line coverage: 100%
Branch coverage
100%
Covered branches: 124
Total branches: 124
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
get_IsOccupied()100%22100%
Initialize(...)100%11100%
Reset()100%88100%
AddOccupant(...)100%44100%
TryRemoveOccupant(...)100%1414100%
GetOccupants()100%66100%
GetConditionalOccupants()100%1414100%
AddOccupantsWithinRadiusTo(...)100%1010100%
AddOccupantsWithinRadius2dTo(...)100%1212100%
AddOccupantsWithinRadiusTo(...)100%1010100%
AddOccupantsWithinRadius2dTo(...)100%1414100%
OccupantPassesFilters(...)100%66100%
IsWithinSquaredRadius(...)100%11100%
TryGetTypedOccupantWithinRadius(...)100%44100%
GetOccupantsFor()100%66100%
TryGetOccupantAt(...)100%1414100%
GetHashCode()100%11100%

File(s)

/home/runner/work/GridForge/GridForge/src/GridForge/Grids/Nodes/ScanCell.cs

#LineLine coverage
 1//=======================================================================
 2// ScanCell.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 System;
 9using System.Collections.Generic;
 10using System.Threading;
 11using FixedMathSharp;
 12using GridForge.Spatial;
 13using SwiftCollections;
 14using SwiftCollections.Utility;
 15
 16namespace GridForge.Grids;
 17
 18/// <summary>
 19/// Stores one occupant registration and the generation that owns its bucket slot.
 20/// </summary>
 21internal readonly struct OccupantEntry
 22{
 23    public readonly IVoxelOccupant Occupant;
 24    public readonly long Generation;
 25
 26    public OccupantEntry(IVoxelOccupant occupant, long generation)
 27    {
 28        Occupant = occupant;
 29        Generation = generation;
 30    }
 31}
 32
 33/// <summary>
 34/// Represents a spatial partition within a grid, managing occupants at a finer granularity than grid voxels.
 35/// Handles efficient tracking, retrieval, and removal of occupants within a designated scan cell area.
 36/// </summary>
 37public class ScanCell
 38{
 39    #region Properties
 40
 41    /// <summary>
 42    /// The world-local index of the grid this scan cell belongs to.
 43    /// </summary>
 44    public ushort GridIndex { get; private set; }
 45
 46    /// <summary>
 47    /// The world that owns this scan cell through its parent grid.
 48    /// </summary>
 49    public GridWorld? World { get; private set; }
 50
 51    /// <summary>
 52    /// A unique identifier for this scan cell in the grid.
 53    /// </summary>
 54    public int CellKey { get; private set; }
 55
 56    /// <summary>
 57    /// Maps a <see cref="Voxel.WorldIndex"/> to a bucket of associated <see cref="IVoxelOccupant"/> instances.
 58    /// </summary>
 59    private SwiftDictionary<WorldVoxelIndex, SwiftBucket<OccupantEntry>>? _voxelOccupants;
 60
 61    private object? _occupantSyncRoot;
 62
 63    private static long s_occupantGenerationCounter;
 64
 65    /// <summary>
 66    /// The total number of occupants in this scan cell.
 67    /// </summary>
 68    public int CellOccupantCount { get; private set; }
 69
 70    /// <summary>
 71    /// Indicates whether this scan cell is currently allocated in the grid.
 72    /// </summary>
 73    public bool IsAllocated { get; private set; }
 74
 75    /// <summary>
 76    /// Determines whether this scan cell is occupied by any occupants.
 77    /// A scan cell is only considered occupied if it is allocated and contains at least one occupant.
 78    /// </summary>
 249379    public bool IsOccupied => IsAllocated && CellOccupantCount > 0;
 80
 81    #endregion
 82
 83    #region Initialization & Reset
 84
 85    /// <summary>
 86    /// Initializes the scan cell with its owning grid and unique cell key.
 87    /// </summary>
 88    internal void Initialize(VoxelGrid grid, int cellKey)
 89    {
 212390        World = grid.World;
 212391        GridIndex = grid.GridIndex;
 212392        CellKey = cellKey;
 212393        IsAllocated = true;
 212394        Volatile.Write(ref _occupantSyncRoot, grid.OccupantSyncRoot);
 212395    }
 96
 97    /// <summary>
 98    /// Resets the scan cell, clearing all occupants and returning memory to object pools.
 99    /// This effectively marks the scan cell as deallocated and removes all references.
 100    /// </summary>
 101    internal void Reset()
 102    {
 2124103        object? syncRoot = Interlocked.Exchange(ref _occupantSyncRoot, null);
 2124104        if (syncRoot == null)
 1105            return;
 106
 2123107        lock (syncRoot)
 108        {
 2123109            if (_voxelOccupants != null)
 110            {
 518111                foreach (var kvp in _voxelOccupants)
 112                {
 188113                    SwiftBucket<OccupantEntry> bucket = kvp.Value;
 1402114                    foreach (OccupantEntry entry in bucket)
 513115                        GridOccupantManager.ForgetTrackedOccupancy(World, entry.Occupant, kvp.Key);
 116
 188117                    Pools.VoxelOccupantBucketPool.Release(bucket);
 118                }
 119
 71120                Pools.VoxelOccupantDictionaryPool.Release(_voxelOccupants);
 71121                _voxelOccupants = null;
 122            }
 123
 2123124            CellOccupantCount = 0;
 125
 2123126            World = null;
 2123127            GridIndex = ushort.MaxValue;
 2123128            CellKey = byte.MaxValue;
 129
 2123130            IsAllocated = false;
 2123131        }
 2123132    }
 133
 134    #endregion
 135
 136    #region Occupant Management
 137
 138    /// <summary>
 139    /// Adds an occupant to this scan cell and tracks its presence.
 140    /// </summary>
 141    /// <param name="index">The global index of the voxel where the occupant resides.</param>
 142    /// <param name="occupant">The occupant instance to add.</param>
 143    /// <returns>A generation-aware ticket for the occupant's bucket slot.</returns>
 144    internal OccupantTicket AddOccupant(WorldVoxelIndex index, IVoxelOccupant occupant)
 145    {
 674146        long generation = RuntimeIdentityAllocator.Allocate(ref s_occupantGenerationCounter);
 673147        _voxelOccupants ??= Pools.VoxelOccupantDictionaryPool.Rent();
 673148        if (!_voxelOccupants.TryGetValue(index, out SwiftBucket<OccupantEntry> bucket))
 149        {
 212150            bucket = Pools.VoxelOccupantBucketPool.Rent();
 212151            _voxelOccupants[index] = bucket;
 152        }
 153
 673154        int slot = bucket.Add(new OccupantEntry(occupant, generation));
 673155        CellOccupantCount++;
 673156        return new OccupantTicket(slot, generation);
 157    }
 158
 159    /// <summary>
 160    /// Removes an occupant from this scan cell.
 161    /// </summary>
 162    /// <param name="index">The global index of the voxel the occupant was assigned to.</param>
 163    /// <param name="ticket">The ticket assigned to the occupant instance from this scancell.</param>
 164    /// <returns>True if the occupant was successfully removed; otherwise, false.</returns>
 165    internal bool TryRemoveOccupant(
 166        WorldVoxelIndex index,
 167        OccupantTicket ticket)
 168    {
 165169        if (!IsOccupied)
 1170            return false;
 171
 164172        if (!_voxelOccupants!.TryGetValue(index, out var bucket))
 1173            return false;
 174
 163175        if (!ticket.IsValid
 163176            || !bucket.TryGetValue(ticket.Slot, out OccupantEntry entry)
 163177            || entry.Generation != ticket.Generation
 163178            || !bucket.TryRemoveAt(ticket.Slot))
 179        {
 3180            return false;
 181        }
 182
 183        // If the occupant was the last in its bucket, remove the entire bucket
 160184        if (bucket.Count == 0)
 185        {
 24186            _voxelOccupants.Remove(index);
 24187            Pools.VoxelOccupantBucketPool.Release(bucket);
 188        }
 189
 160190        CellOccupantCount--;
 191
 160192        return true;
 193    }
 194
 195    #endregion
 196
 197    #region Occupant Retrieval
 198
 199    /// <summary>
 200    /// Retrieves all occupants associated with this ScanCell.
 201    /// </summary>
 202    /// <returns>An enumerable of occupants within this scan cell.</returns>
 203    public IEnumerable<IVoxelOccupant> GetOccupants()
 204    {
 10205        if (_voxelOccupants == null)
 2206            yield break;
 207
 30208        foreach (SwiftBucket<OccupantEntry> bucket in _voxelOccupants.Values)
 209        {
 162210            foreach (OccupantEntry entry in bucket)
 74211                yield return entry.Occupant;
 212        }
 6213    }
 214
 215    /// <summary>
 216    /// Retrieves occupants whose group Ids match a given condition.
 217    /// </summary>
 218    public IEnumerable<IVoxelOccupant> GetConditionalOccupants(
 219        Func<IVoxelOccupant, bool>? occupantCondition = null,
 220        Func<byte, bool>? groupConditional = null)
 221    {
 9222        if (_voxelOccupants == null)
 1223            yield break;
 224
 225        // Loop through each voxel's bucket and filter by the cluster condition
 31226        foreach (var bucket in _voxelOccupants.Values)
 227        {
 43228            foreach (OccupantEntry entry in bucket)
 229            {
 14230                IVoxelOccupant occupant = entry.Occupant;
 14231                if (occupantCondition != null && !occupantCondition(occupant))
 232                    continue;
 233
 11234                if (groupConditional != null && !groupConditional(occupant.OccupantGroupId))
 235                    continue;
 236
 7237                yield return occupant;
 238            }
 239        }
 7240    }
 241
 242    /// <summary>
 243    /// Appends occupants within the squared radius to caller-owned storage without allocating an iterator.
 244    /// </summary>
 245    internal void AddOccupantsWithinRadiusTo(
 246        SwiftList<IVoxelOccupant> results,
 247        Vector3d position,
 248        Fixed64 squaredRadius,
 249        Func<IVoxelOccupant, bool>? occupantCondition = null,
 250        Func<byte, bool>? groupCondition = null)
 251    {
 273252        if (_voxelOccupants == null)
 1253            return;
 254
 33484255        foreach (var kvp in _voxelOccupants)
 256        {
 16470257            SwiftBucket<OccupantEntry> bucket = kvp.Value;
 65882258            foreach (OccupantEntry entry in bucket)
 259            {
 16471260                IVoxelOccupant occupant = entry.Occupant;
 16471261                if (OccupantPassesFilters(occupant, occupantCondition, groupCondition)
 16471262                    && IsWithinSquaredRadius(occupant, position, squaredRadius))
 263                {
 16461264                    results.Add(occupant);
 265                }
 266            }
 267        }
 272268    }
 269
 270    /// <summary>
 271    /// Appends occupants within the XZ squared radius on the selected local Y voxel layer.
 272    /// </summary>
 273    internal void AddOccupantsWithinRadius2dTo(
 274        SwiftList<IVoxelOccupant> results,
 275        Vector3d position,
 276        int localLayerY,
 277        Fixed64 squaredRadius,
 278        Func<IVoxelOccupant, bool>? occupantCondition = null,
 279        Func<byte, bool>? groupCondition = null)
 280    {
 264281        if (_voxelOccupants == null)
 1282            return;
 283
 33440284        foreach (var kvp in _voxelOccupants)
 285        {
 16457286            if (kvp.Key.VoxelIndex.y != localLayerY)
 287                continue;
 288
 16453289            SwiftBucket<OccupantEntry> bucket = kvp.Value;
 65816290            foreach (OccupantEntry entry in bucket)
 291            {
 16455292                IVoxelOccupant occupant = entry.Occupant;
 16455293                if (OccupantPassesFilters(occupant, occupantCondition, groupCondition)
 16455294                    && GridPlane2d.DistanceSquaredXZ(occupant.Position, position) <= squaredRadius)
 295                {
 16452296                    results.Add(occupant);
 297                }
 298            }
 299        }
 263300    }
 301
 302    /// <summary>
 303    /// Appends typed occupants within the squared radius to caller-owned storage without LINQ.
 304    /// </summary>
 305    internal void AddOccupantsWithinRadiusTo<T>(
 306        SwiftList<T> results,
 307        Vector3d position,
 308        Fixed64 squaredRadius,
 309        Func<IVoxelOccupant, bool>? occupantCondition = null,
 310        Func<byte, bool>? groupCondition = null) where T : IVoxelOccupant
 311    {
 4312        if (_voxelOccupants == null)
 1313            return;
 314
 12315        foreach (var kvp in _voxelOccupants)
 316        {
 3317            SwiftBucket<OccupantEntry> bucket = kvp.Value;
 14318            foreach (OccupantEntry entry in bucket)
 319            {
 4320                IVoxelOccupant occupant = entry.Occupant;
 4321                if (!OccupantPassesFilters(occupant, occupantCondition, groupCondition))
 322                    continue;
 323
 3324                if (TryGetTypedOccupantWithinRadius(occupant, position, squaredRadius, out T typedOccupant))
 2325                    results.Add(typedOccupant);
 326            }
 327        }
 3328    }
 329
 330    /// <summary>
 331    /// Appends typed occupants within the XZ squared radius on the selected local Y voxel layer.
 332    /// </summary>
 333    internal void AddOccupantsWithinRadius2dTo<T>(
 334        SwiftList<T> results,
 335        Vector3d position,
 336        int localLayerY,
 337        Fixed64 squaredRadius,
 338        Func<IVoxelOccupant, bool>? occupantCondition = null,
 339        Func<byte, bool>? groupCondition = null) where T : IVoxelOccupant
 340    {
 5341        if (_voxelOccupants == null)
 1342            return;
 343
 20344        foreach (var kvp in _voxelOccupants)
 345        {
 6346            if (kvp.Key.VoxelIndex.y != localLayerY)
 347                continue;
 348
 4349            SwiftBucket<OccupantEntry> bucket = kvp.Value;
 20350            foreach (OccupantEntry entry in bucket)
 351            {
 6352                IVoxelOccupant occupant = entry.Occupant;
 6353                if (!OccupantPassesFilters(occupant, occupantCondition, groupCondition))
 354                    continue;
 355
 4356                if (occupant is T typedOccupant
 4357                    && GridPlane2d.DistanceSquaredXZ(occupant.Position, position) <= squaredRadius)
 358                {
 2359                    results.Add(typedOccupant);
 360                }
 361            }
 362        }
 4363    }
 364
 365    private static bool OccupantPassesFilters(
 366        IVoxelOccupant occupant,
 367        Func<IVoxelOccupant, bool>? occupantCondition,
 368        Func<byte, bool>? groupCondition)
 369    {
 32936370        return (occupantCondition == null || occupantCondition(occupant))
 32936371            && (groupCondition == null || groupCondition(occupant.OccupantGroupId));
 372    }
 373
 374    private static bool IsWithinSquaredRadius(
 375        IVoxelOccupant occupant,
 376        Vector3d position,
 377        Fixed64 squaredRadius)
 378    {
 16468379        return (occupant.Position - position).MagnitudeSquared <= squaredRadius;
 380    }
 381
 382    private static bool TryGetTypedOccupantWithinRadius<T>(
 383        IVoxelOccupant occupant,
 384        Vector3d position,
 385        Fixed64 squaredRadius,
 386        out T typedOccupant) where T : IVoxelOccupant
 387    {
 3388        typedOccupant = default!;
 3389        if (occupant is not T candidate || !IsWithinSquaredRadius(occupant, position, squaredRadius))
 1390            return false;
 391
 2392        typedOccupant = candidate;
 2393        return true;
 394    }
 395
 396    /// <summary>
 397    /// Retrieves all occupants associated with a specific voxel spawn token within this scan cell.
 398    /// </summary>
 399    /// <param name="index">The global index of the voxel.</param>
 400    /// <returns>An enumerable collection of occupants assigned to the voxel.</returns>
 401    public IEnumerable<IVoxelOccupant> GetOccupantsFor(WorldVoxelIndex index)
 402    {
 6403        if (_voxelOccupants == null || !_voxelOccupants.TryGetValue(index, out SwiftBucket<OccupantEntry> voxelOccupants
 2404            yield break;
 405
 526406        foreach (OccupantEntry entry in voxelOccupants)
 259407            yield return entry.Occupant;
 4408    }
 409
 410    /// <summary>
 411    /// Attempts to retrieve a specific occupant in this scan cell using a voxel's spawn key and occupant ticket.
 412    /// </summary>
 413    /// <param name="index">The global index of the voxel the occupant belongs to.</param>
 414    /// <param name="occupantTicket">The unique ticket identifying the occupant.</param>
 415    /// <param name="voxelOccupant">The retrieved occupant if found.</param>
 416    /// <returns>True if the occupant was found, otherwise false.</returns>
 417    public bool TryGetOccupantAt(
 418        WorldVoxelIndex index,
 419        OccupantTicket occupantTicket,
 420        out IVoxelOccupant? voxelOccupant)
 421    {
 8219422        voxelOccupant = null;
 8219423        object? syncRoot = Volatile.Read(ref _occupantSyncRoot);
 8219424        if (syncRoot == null)
 1425            return false;
 426
 8218427        lock (syncRoot)
 428        {
 8218429            if (!ReferenceEquals(syncRoot, Volatile.Read(ref _occupantSyncRoot))
 8218430                || _voxelOccupants == null
 8218431                || !_voxelOccupants.TryGetValue(index, out SwiftBucket<OccupantEntry> voxelOccupants)
 8218432                || !occupantTicket.IsValid
 8218433                || !voxelOccupants.TryGetValue(occupantTicket.Slot, out OccupantEntry entry)
 8218434                || entry.Generation != occupantTicket.Generation)
 435            {
 9436                return false;
 437            }
 438
 8209439            voxelOccupant = entry.Occupant;
 8209440            return true;
 441        }
 8218442    }
 443
 444    #endregion
 445
 446    /// <inheritdoc/>
 2302447    public override int GetHashCode() => SwiftHashTools.CombineHashCodes(GridIndex, CellKey);
 448}

Methods/Properties

get_IsOccupied()
Initialize(GridForge.Grids.VoxelGrid,System.Int32)
Reset()
AddOccupant(GridForge.Spatial.WorldVoxelIndex,GridForge.Spatial.IVoxelOccupant)
TryRemoveOccupant(GridForge.Spatial.WorldVoxelIndex,GridForge.OccupantTicket)
GetOccupants()
GetConditionalOccupants()
AddOccupantsWithinRadiusTo(SwiftCollections.SwiftList`1<GridForge.Spatial.IVoxelOccupant>,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,System.Func`2<GridForge.Spatial.IVoxelOccupant,System.Boolean>,System.Func`2<System.Byte,System.Boolean>)
AddOccupantsWithinRadius2dTo(SwiftCollections.SwiftList`1<GridForge.Spatial.IVoxelOccupant>,FixedMathSharp.Vector3d,System.Int32,FixedMathSharp.Fixed64,System.Func`2<GridForge.Spatial.IVoxelOccupant,System.Boolean>,System.Func`2<System.Byte,System.Boolean>)
AddOccupantsWithinRadiusTo(SwiftCollections.SwiftList`1<T>,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,System.Func`2<GridForge.Spatial.IVoxelOccupant,System.Boolean>,System.Func`2<System.Byte,System.Boolean>)
AddOccupantsWithinRadius2dTo(SwiftCollections.SwiftList`1<T>,FixedMathSharp.Vector3d,System.Int32,FixedMathSharp.Fixed64,System.Func`2<GridForge.Spatial.IVoxelOccupant,System.Boolean>,System.Func`2<System.Byte,System.Boolean>)
OccupantPassesFilters(GridForge.Spatial.IVoxelOccupant,System.Func`2<GridForge.Spatial.IVoxelOccupant,System.Boolean>,System.Func`2<System.Byte,System.Boolean>)
IsWithinSquaredRadius(GridForge.Spatial.IVoxelOccupant,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64)
TryGetTypedOccupantWithinRadius(GridForge.Spatial.IVoxelOccupant,FixedMathSharp.Vector3d,FixedMathSharp.Fixed64,T&)
GetOccupantsFor()
TryGetOccupantAt(GridForge.Spatial.WorldVoxelIndex,GridForge.OccupantTicket,GridForge.Spatial.IVoxelOccupant&)
GetHashCode()