< Summary

Information
Class: Trailblazer.Pathing.SolidChartPartition
Assembly: Trailblazer
File(s): /home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Partition/SolidChartPartition.cs
Line coverage
95%
Covered lines: 136
Uncovered lines: 7
Coverable lines: 143
Total lines: 487
Line coverage: 95.1%
Branch coverage
87%
Covered branches: 68
Total branches: 78
Branch coverage: 87.1%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
get_ClearanceQueuePool()100%11100%
get_Voxel()50%4475%
get_PathCostModifier()100%11100%
set_PathCostModifier(...)100%11100%
.ctor()100%11100%
get_HasAnyOwners()100%22100%
SetParentIndex(...)100%11100%
SetOwner(...)100%11100%
OnAddToVoxel(...)100%11100%
OnRemoveFromVoxel(...)50%2271.42%
Reset()100%22100%
HandleChange(...)100%22100%
BindNeighbors()70%131070%
GetNeighborClearance()100%11100%
IsImpassable(...)100%44100%
CheckClearance()90%1010100%
TryGetClearanceOrigin(...)50%22100%
TryGetGridAndVoxel(...)100%11100%
RequireOwnerState()100%22100%
ComputeClearanceRadius()100%44100%
ExploreClearanceNeighbors(...)90%101091.66%
IsClearanceBoundary(...)100%88100%
ShouldExpandClearanceSearch(...)100%66100%
ApplyAuthoredState(...)75%44100%
BelongsTo(...)100%22100%
SetReachabilityComponent(...)100%11100%
TryGetReachabilityComponent(...)100%44100%
GetHashCode()100%11100%

File(s)

/home/runner/work/Trailblazer/Trailblazer/src/Trailblazer/Pathing/Partition/SolidChartPartition.cs

#LineLine coverage
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using GridForge.Spatial;
 4using SwiftCollections;
 5using SwiftCollections.Pool;
 6using System;
 7using System.Diagnostics.CodeAnalysis;
 8using System.Runtime.CompilerServices;
 9
 10namespace Trailblazer.Pathing;
 11
 12/// <summary>
 13/// Represents a partition attached to a Voxel that provides additional data used during pathfinding,
 14/// such as clearance information, movement cost, and neighbor traversal helpers.
 15/// </summary>
 16public class SolidChartPartition : IVoxelPartition
 17{
 18    #region Constants
 19
 20    /// <summary>
 21    /// Maximum clearance degree allowed for valid traversal.
 22    /// </summary>
 123    public static readonly byte DefaultDegreeCap = 8;
 24
 125    private static readonly Lazy<SwiftQueuePool<(SolidChartPartition v, byte dist)>> _clearanceQueuePool =
 126        new(() => new SwiftQueuePool<(SolidChartPartition v, byte dist)>());
 27
 72028    internal static SwiftQueuePool<(SolidChartPartition v, byte dist)> ClearanceQueuePool => _clearanceQueuePool.Value;
 29
 30    #endregion
 31
 32    /// <summary>
 33    /// The world-scoped coordinate of the voxel this partition is attached to.
 34    /// </summary>
 35    public WorldVoxelIndex WorldIndex { get; private set; }
 36
 37    internal PathingWorldState? OwnerState { get; private set; }
 38
 39    /// <summary>
 40    /// Gets the voxel associated with this partition.
 41    /// </summary>
 42    public Voxel Voxel
 43    {
 44        get
 45        {
 177446            if (TryGetGridAndVoxel(WorldIndex, out _, out Voxel? voxel)
 177447                && voxel != null)
 177348                return voxel;
 049            throw new InvalidOperationException($"Partition at {WorldIndex} is not attached to a valid voxel!");
 50        }
 51    }
 52
 53    /// <summary>
 54    /// The world-space position of the voxel.
 55    /// </summary>
 56    public Vector3d VoxelPosition { get; private set; }
 57
 58    /// <summary>
 59    /// Gets a value indicating whether the current tile can be traversed.
 60    /// </summary>
 61    public bool IsWalkable { get; private set; }
 62
 63    /// <summary>
 64    /// Indicates whether the voxel has been partitioned and is in use.
 65    /// </summary>
 66    public bool IsPartitioned { get; set; }
 67
 68    /// <summary>
 69    /// A cost bias for this partition. Positive values make the partition less desirable.
 70    /// The public setter preserves caller-controlled adjustments,
 71    /// while chart-authored modifiers are aggregated separately.
 72    /// </summary>
 73    public int PathCostModifier
 74    {
 75        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 1327476        get => _manualPathCostModifier + _chartPathCostModifier;
 77        [MethodImpl(MethodImplOptions.AggressiveInlining)]
 244678        set => _manualPathCostModifier = value;
 79    }
 80
 81    private int _manualPathCostModifier;
 82
 83    private int _chartPathCostModifier;
 84
 85    /// <summary>
 86    /// Gets the neighboring partitions adjacent to this partition.
 87    /// </summary>
 88    /// <remarks>
 89    /// Each element in the array represents a neighboring partition in a specific direction or position.
 90    /// The array may contain null values if a neighbor does not exist in that position.
 91    /// </remarks>
 92    public SolidChartPartition?[]? Neighbors { get; private set; }
 93
 94    #region Clearance Properties
 95
 96    /// <summary>
 97    /// The number of traversable connections until the nearest unwalkable voxel.
 98    /// </summary>
 99    private byte _clearanceRadiusInVoxels;
 100
 101    /// <summary>
 102    /// Indicates whether the clearance degree has been computed and is valid.
 103    /// </summary>
 104    private bool _isClearanceValid;
 105
 106    #endregion
 107
 108    #region Reachability Snapshot
 109
 110    private int _reachabilitySnapshotKey;
 111
 2393112    private int _reachabilityVersion = -1;
 113
 114    private int _reachabilityComponentId;
 115
 116    #endregion
 117
 118    #region Chart Properties
 119
 120    /// <summary>
 121    /// Maps that currently include this partition as part of their traversable space.
 122    /// </summary>
 123    public SwiftHashSet<string>? ChartOwners { get; private set; }
 124
 125    /// <summary>
 126    /// The chart whose authored cell currently wins overlap resolution for this voxel.
 127    /// </summary>
 128    public string? EffectiveChartOwner { get; private set; }
 129
 130    /// <summary>
 131    /// The authored chart flags from the winning effective cell currently applied to this live partition.
 132    /// </summary>
 133    public NavigationChartCellFlags ChartFlags { get; private set; }
 134
 135    /// <summary>
 136    /// Returns true if any map currently references this partition.
 137    /// </summary>
 5138    public bool HasAnyOwners => ChartOwners?.Count > 0;
 139
 140    #endregion
 141
 142    /// <summary>
 143    /// Sets the parent index for the current voxel in the world.
 144    /// </summary>
 145    /// <param name="parentIndex">The index to assign as the parent of the current voxel.</param>
 2442146    public void SetParentIndex(WorldVoxelIndex parentIndex) => WorldIndex = parentIndex;
 147
 2442148    internal void SetOwner(PathingWorldState ownerState) => OwnerState = ownerState;
 149
 150    /// <summary>
 151    /// Attaches a partition to a specified <see cref="Voxel"/>, updating its state and invoking initialization logic.
 152    /// </summary>
 153    /// <param name="voxel">The target voxel where the partition will be added.</param>
 154    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 155    public void OnAddToVoxel(Voxel voxel)
 156    {
 2456157        voxel.OnObstacleAdded += HandleChange;
 2456158        voxel.OnObstacleRemoved += HandleChange;
 159
 2456160        WorldIndex = voxel.WorldIndex;
 2456161        VoxelPosition = voxel.WorldPosition;
 162
 2456163        IsWalkable = !voxel.IsBlocked;
 164
 2456165        _clearanceRadiusInVoxels = DefaultDegreeCap;
 166
 2456167        IsPartitioned = true;
 2456168    }
 169
 170    /// <summary>
 171    /// Detaches a partition from a specified <see cref="Voxel"/>, resetting its state and invoking cleanup logic.
 172    /// </summary>
 173    /// <param name="voxel">The target voxel from which the partition will be removed.</param>
 174    /// <remarks>
 175    /// This will call <see cref="Reset"/> as an action on release
 176    /// </remarks>
 177    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 178    public void OnRemoveFromVoxel(Voxel voxel)
 179    {
 2442180        voxel.OnObstacleAdded -= HandleChange;
 2442181        voxel.OnObstacleRemoved -= HandleChange;
 182
 2442183        PathingWorldState? ownerState = OwnerState;
 2442184        if (ownerState != null)
 2442185            ownerState.PartitionPool.Release(this);
 186        else
 0187            PathManager.PartitionPool.Release(this);
 0188    }
 189
 190    /// <summary>
 191    /// Resets this partition's internal state, preparing it for reuse or reattachment.
 192    /// </summary>
 193    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 194    internal void Reset()
 195    {
 2444196        WorldIndex = default;
 2444197        OwnerState = null;
 198
 2444199        _isClearanceValid = false;
 200
 2444201        IsWalkable = false;
 202
 2444203        PathCostModifier = 0;
 2444204        _chartPathCostModifier = 0;
 2444205        ChartFlags = NavigationChartCellFlags.None;
 206
 2444207        Neighbors = null;
 208
 2444209        _clearanceRadiusInVoxels = DefaultDegreeCap;
 210
 2444211        ChartOwners?.Clear();
 2444212        EffectiveChartOwner = null;
 213
 2444214        _reachabilitySnapshotKey = 0;
 2444215        _reachabilityVersion = -1;
 2444216        _reachabilityComponentId = 0;
 217
 2444218        IsPartitioned = false;
 2444219    }
 220
 221    /// <summary>
 222    /// Handles any obstacle changes on the associated voxel and invalidates clearance as needed.
 223    /// </summary>
 224    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 225    public void HandleChange(ObstacleEventInfo eventInfo)
 226    {
 227        // regardless of change type, we need to update clearance
 228
 7229        IsWalkable = eventInfo.VoxelIndex != default && eventInfo.ObstacleCount == 0;
 7230        _clearanceRadiusInVoxels = DefaultDegreeCap;
 7231        _isClearanceValid = false;
 7232        SolidPartitionReachability.Invalidate(RequireOwnerState());
 7233    }
 234
 235    /// <summary>
 236    /// Populates the Neighbors array with references to adjacent SolidChartPartition instances based on the current Wor
 237    /// </summary>
 238    /// <remarks>
 239    /// If the grid or voxel corresponding to WorldIndex cannot be found, Neighbors is set to null.
 240    /// Each entry in the Neighbors array corresponds to a spatial direction;
 241    /// entries remain null if a neighbor is blocked or missing.
 242    /// </remarks>
 243    public void BindNeighbors()
 244    {
 2483245        Neighbors = new SolidChartPartition?[26];
 246
 2483247        if (!TryGetGridAndVoxel(WorldIndex, out VoxelGrid? grid, out Voxel? voxel))
 248        {
 0249            TrailblazerLogger.Channel.Warn($"Failed to find grid or voxel for WorldIndex {WorldIndex}. Neighbors will be
 0250            Neighbors = null;
 0251            return;
 252        }
 253
 254        // for each of the 26 SpatialDirection values (except None)
 134082255        foreach (SpatialDirection dir in SpatialAwareness.AllDirections)
 256        {
 257            // use Voxel’s cached neighbor lookup
 64558258            if (voxel!.TryGetNeighborFromDirection(grid!, dir, out Voxel? neighborVoxel, useCache: true)
 64558259             && neighborVoxel!.TryGetPartition(out SolidChartPartition? neighborPart))
 260            {
 11267261                Neighbors[(int)dir] = neighborPart;
 262            }
 263            // else leave null = “blocked or missing”
 264        }
 2483265    }
 266
 267    /// <summary>
 268    /// Returns the cached or recalculated clearance value to nearby obstacles.
 269    /// </summary>
 270    public byte GetNeighborClearance()
 271    {
 223272        CheckClearance();
 223273        return _clearanceRadiusInVoxels;
 274    }
 275
 276    /// <summary>
 277    /// If this unit is too fat to fit.
 278    /// </summary>
 279    internal bool IsImpassable(Fixed64 unitSize)
 280    {
 22153281        if (unitSize <= Fixed64.Zero)
 2282            return false;
 283
 22151284        PathingWorldState ownerState = RequireOwnerState();
 22151285        Fixed64 voxelSize = ownerState.World.VoxelSize;
 22151286        if (unitSize <= voxelSize)
 21732287            return !IsWalkable;
 288
 289        // Only evaluates local radial clearance from current voxel.
 290        // Does not account for directional corner blocking
 419291        CheckClearance();
 292
 293        // How many voxels wide our agent is, in cell terms
 418294        int required = (unitSize / voxelSize).CeilToInt();
 295        // If there aren't at least that many free voxels around, it can't go
 296
 418297        return required > _clearanceRadiusInVoxels;
 298    }
 299
 300    /// <summary>
 301    /// Validates or recalculates the clearance degree from nearby voxels.
 302    /// </summary>
 303    private void CheckClearance()
 304    {
 642305        if (Neighbors == null)
 1306            throw new InvalidOperationException("Must call BindNeighbors() before clearance.");
 307
 641308        if (_isClearanceValid)
 279309            return;
 310
 362311        _isClearanceValid = true;
 312
 362313        if (!TryGetClearanceOrigin(out Voxel? origin))
 314        {
 2315            _clearanceRadiusInVoxels = origin != null && origin.IsBlocked ? (byte)0 : DefaultDegreeCap;
 2316            return;
 317        }
 318
 360319        _clearanceRadiusInVoxels = ComputeClearanceRadius();
 360320    }
 321
 322    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 323    private bool TryGetClearanceOrigin([MaybeNullWhen(false)] out Voxel origin)
 324    {
 362325        return TryGetGridAndVoxel(WorldIndex, out _, out origin)
 362326            && IsWalkable;
 327    }
 328
 329    private bool TryGetGridAndVoxel(
 330        WorldVoxelIndex voxelIndex,
 331        out VoxelGrid? grid,
 332        out Voxel? voxel)
 333    {
 4619334        return RequireOwnerState().World.TryGetGridAndVoxel(voxelIndex, out grid, out voxel);
 335    }
 336
 337    private PathingWorldState RequireOwnerState() =>
 26777338        OwnerState ?? throw new InvalidOperationException("Solid chart partition requires an owning pathing context.");
 339
 340    private byte ComputeClearanceRadius()
 341    {
 342        // BFS from this voxel until we hit any blocked-or-missing neighbor
 360343        byte best = DefaultDegreeCap;
 360344        SwiftQueue<(SolidChartPartition v, byte dist)> q = ClearanceQueuePool.Rent();
 360345        SwiftHashSet<SolidChartPartition> visited = PathManager.PartitionSetPool.Rent();
 346
 347        try
 348        {
 360349            q.Enqueue((this, 0));
 360350            visited.Add(this);
 351
 352            // stop BFS either when queue empty or we’ve already found best=1
 2881353            while (q.Count > 0 && best > 1)
 354            {
 2521355                (SolidChartPartition part, byte dist) = q.Dequeue();
 2521356                ExploreClearanceNeighbors(part, dist, visited, q, ref best);
 357            }
 358
 359            // clamp to cap so you never return > DefaultDegreeCap
 360360            return Math.Min(best, DefaultDegreeCap);
 361        }
 362        finally
 363        {
 360364            ClearanceQueuePool.Release(q);
 360365            PathManager.PartitionSetPool.Release(visited);
 360366        }
 360367    }
 368
 369    private static void ExploreClearanceNeighbors(
 370        SolidChartPartition part,
 371        byte dist,
 372        SwiftHashSet<SolidChartPartition> visited,
 373        SwiftQueue<(SolidChartPartition v, byte dist)> queue,
 374        ref byte best)
 375    {
 2521376        SolidChartPartition?[]? neighbors = part.Neighbors;
 2521377        if (neighbors == null)
 0378            return;
 379
 136134380        for (int i = 0; i < neighbors.Length; i++)
 381        {
 65546382            byte nextDist = (byte)(dist + 1);
 65546383            SolidChartPartition? neighbor = neighbors[i];
 384
 65546385            if (IsClearanceBoundary(i, neighbor))
 386            {
 3364387                best = Math.Min(best, nextDist);
 3364388                continue;
 389            }
 390
 62182391            if (neighbor != null && ShouldExpandClearanceSearch(nextDist, best, neighbor, visited))
 2550392                queue.Enqueue((neighbor, nextDist));
 393        }
 2521394    }
 395
 396    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 397    private static bool IsClearanceBoundary(int neighborIndex, SolidChartPartition? neighbor)
 398    {
 65546399        if (neighbor != null && neighbor.IsWalkable)
 16810400            return false;
 401
 402        // skip above, below, or any above/below diagonals
 48736403        return neighborIndex != 4 && neighborIndex != 5 && neighborIndex < 10;
 404    }
 405
 406    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 407    private static bool ShouldExpandClearanceSearch(
 408        byte nextDist,
 409        byte best,
 410        SolidChartPartition? neighbor,
 411        SwiftHashSet<SolidChartPartition> visited)
 412    {
 16810413        return neighbor != null
 16810414            && nextDist < best
 16810415            && nextDist < DefaultDegreeCap
 16810416            && visited.Add(neighbor);
 417    }
 418
 419    #region NavigationChart Management
 420
 421    /// <summary>
 422    /// Applies the resolved overlap state for this voxel to the active solid partition.
 423    /// </summary>
 424    internal void ApplyAuthoredState(
 425        ResolvedChartVoxelState? state,
 426        string? effectiveChartOwner,
 427        NavigationChartCell effectiveCell)
 428    {
 2457429        ChartOwners ??= new SwiftHashSet<string>();
 2457430        ChartOwners.Clear();
 2457431        state?.AddChartOwnersTo(ChartOwners);
 432
 2457433        EffectiveChartOwner = effectiveChartOwner;
 2457434        _chartPathCostModifier = effectiveCell.PathCostModifier;
 2457435        ChartFlags = effectiveCell.Flags;
 2457436    }
 437
 438    /// <summary>
 439    /// Returns true if the partition is claimed by the given map name.
 440    /// </summary>
 441    [MethodImpl(MethodImplOptions.AggressiveInlining)]
 11442    public bool BelongsTo(string mapName) => ChartOwners?.Contains(mapName) == true;
 443
 444    #endregion
 445
 446    /// <summary>
 447    /// Records the reachability component assigned by the latest matching solid-partition snapshot.
 448    /// </summary>
 449    internal void SetReachabilityComponent(int snapshotKey, int version, int componentId)
 450    {
 822451        _reachabilitySnapshotKey = snapshotKey;
 822452        _reachabilityVersion = version;
 822453        _reachabilityComponentId = componentId;
 822454    }
 455
 456    /// <summary>
 457    /// Tries to read the component recorded for the requested solid-partition reachability snapshot.
 458    /// </summary>
 459    internal bool TryGetReachabilityComponent(int snapshotKey, int version, out int componentId)
 460    {
 3392461        if (_reachabilitySnapshotKey == snapshotKey && _reachabilityVersion == version)
 462        {
 3368463            componentId = _reachabilityComponentId;
 3368464            return true;
 465        }
 466
 24467        componentId = 0;
 24468        return false;
 469    }
 470
 471    /// <inheritdoc/>
 472    public override int GetHashCode()
 473    {
 474        unchecked
 475        {
 133978476            VoxelIndex voxelIndex = WorldIndex.VoxelIndex;
 133978477            int hash = 17;
 133978478            hash = (hash * 31) + WorldIndex.WorldSpawnToken;
 133978479            hash = (hash * 31) + WorldIndex.GridIndex;
 133978480            hash = (hash * 31) + WorldIndex.GridSpawnToken;
 133978481            hash = (hash * 31) + voxelIndex.x;
 133978482            hash = (hash * 31) + voxelIndex.y;
 133978483            hash = (hash * 31) + voxelIndex.z;
 133978484            return hash;
 485        }
 486    }
 487}