< Summary

Information
Class: GridForge.Blockers.Blocker
Assembly: GridForge
File(s): /home/runner/work/GridForge/GridForge/src/GridForge/Blockers/Blocker.cs
Line coverage
100%
Covered lines: 203
Uncovered lines: 0
Coverable lines: 203
Total lines: 520
Line coverage: 100%
Branch coverage
100%
Covered branches: 112
Total branches: 112
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/GridForge/GridForge/src/GridForge/Blockers/Blocker.cs

#LineLine coverage
 1//=======================================================================
 2// Blocker.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 FixedMathSharp;
 10using GridForge.Grids;
 11using GridForge.Spatial;
 12using GridForge.Utility;
 13using SwiftCollections;
 14using SwiftCollections.Pool;
 15
 16namespace GridForge.Blockers;
 17
 18/// <summary>
 19/// Base class for all grid blockers that handles applying and removing obstacles.
 20/// </summary>
 21public abstract class Blocker : IBlocker
 22{
 15323    private readonly object _gridWatcherLock = new();
 24    private bool _isWatchingWorldEvents;
 25
 26    /// <summary>
 27    /// The world this blocker is bound to.
 28    /// </summary>
 29    public GridWorld World { get; }
 30
 31    /// <summary>
 32    /// Unique token representing this blockage instance.
 33    /// </summary>
 34    public ObstacleToken BlockageToken { get; private set; }
 35
 36    /// <summary>
 37    /// Indicates whether the blocker is currently active.
 38    /// </summary>
 39    public bool IsActive { get; protected set; }
 40
 41    /// <summary>
 42    /// The cached minimum bounds of the blockage area.
 43    /// </summary>
 44    public Vector3d CacheMin { get; protected set; }
 45
 46    /// <summary>
 47    /// The cached maximum bounds of the blockage area.
 48    /// </summary>
 49    public Vector3d CacheMax { get; private set; }
 50
 51    /// <summary>
 52    /// Tracks whether the blocker is currently blocking voxels.
 53    /// </summary>
 54    public bool IsBlocking { get; protected set; }
 55
 56    /// <summary>
 57    /// Flags whether or not to hold onto a reference of the voxels this blocker covers.
 58    /// </summary>
 59    public bool CacheCoveredVoxels { get; protected set; }
 60
 61    /// <summary>
 62    /// Stable voxel identifiers cached for safe blocker removal when <see cref="CacheCoveredVoxels"/> is true.
 63    /// </summary>
 64    protected SwiftList<WorldVoxelIndex>? _cachedCoveredVoxels;
 65
 66    /// <summary>
 67    /// Grid indices currently covered by this blocker.
 68    /// </summary>
 15369    private readonly SwiftHashSet<ushort> _watchedGridIndices = new();
 70
 71    /// <summary>
 72    /// Event triggered when a blocker is applied.
 73    /// </summary>
 74    private static Action<BlockageEventInfo>? _onBlockageApplied;
 75
 76    /// <inheritdoc cref="_onBlockageApplied"/>
 77    public static event Action<BlockageEventInfo> OnBlockageApplied
 78    {
 379        add => _onBlockageApplied += value;
 380        remove => _onBlockageApplied -= value;
 81    }
 82
 83    /// <summary>
 84    /// Event triggered when a blocker is removed.
 85    /// </summary>
 86    private static Action<BlockageEventInfo>? _onBlockageRemoved;
 87
 88    /// <inheritdoc cref="_onBlockageRemoved"/>
 89    public static event Action<BlockageEventInfo> OnBlockageRemoved
 90    {
 291        add => _onBlockageRemoved += value;
 292        remove => _onBlockageRemoved -= value;
 93    }
 94
 95    /// <summary>
 96    /// Initializes a new blocker instance bound to the supplied world.
 97    /// </summary>
 98    /// <param name="world">The world whose grids this blocker should affect.</param>
 99    /// <param name="active">Flag whether or not this blocker will block on update.</param>
 100    /// <param name="cacheCoveredVoxels">Flag whether or not to cache covered voxels that are blocked.</param>
 153101    protected Blocker(GridWorld world, bool active = true, bool cacheCoveredVoxels = false)
 102    {
 153103        SwiftThrowHelper.ThrowIfNull(world, nameof(world));
 153104        World = world;
 153105        IsActive = active;
 153106        CacheCoveredVoxels = cacheCoveredVoxels;
 153107    }
 108
 109    /// <summary>
 110    /// Toggles the blocker from inactive to active or active to inactive state
 111    /// If object is currently blocking, the blocker will be removed.
 112    /// If object is not active and not blocking, the blocker will be applied.
 113    /// </summary>
 114    public virtual void ToggleStatus(bool status)
 115    {
 3116        if (!status)
 117        {
 1118            RemoveBlockageCore(keepWatching: false, preserveToken: false);
 1119            IsActive = false;
 1120            return;
 121        }
 122
 2123        if (!IsBlocking)
 124        {
 1125            IsActive = true;
 1126            ApplyBlockage();
 127        }
 2128    }
 129
 130    /// <summary>
 131    /// Applies the blockage by marking voxels as obstacles.
 132    /// </summary>
 133    public virtual void ApplyBlockage()
 134    {
 155135        ApplyBlockageCore(preserveTokenWhenEmpty: false);
 155136    }
 137
 138    private void ApplyBlockageCore(bool preserveTokenWhenEmpty)
 139    {
 165140        if (!IsActive || IsBlocking || !World.IsActive)
 3141            return;
 142
 162143        PrepareBlockageApplication();
 162144        ObstacleToken blockageToken = BlockageToken;
 162145        bool foundCoverage = ApplyBlockageToCoveredVoxels(blockageToken, out bool hasCoverage);
 146
 162147        IsBlocking = foundCoverage && hasCoverage;
 148
 162149        if (IsBlocking)
 150        {
 154151            NotifyBlockageApplied();
 154152            return;
 153        }
 154
 8155        if (!preserveTokenWhenEmpty || !hasCoverage)
 5156            BlockageToken = default;
 8157    }
 158
 159    /// <summary>
 160    /// Removes the blockage by clearing obstacle markers from voxels.
 161    /// </summary>
 162    public virtual void RemoveBlockage()
 163    {
 27164        RemoveBlockageCore(keepWatching: false, preserveToken: false);
 27165    }
 166
 167    private void RemoveBlockageCore(bool keepWatching, bool preserveToken)
 168    {
 39169        if (!IsBlocking)
 170        {
 7171            if (!preserveToken)
 2172                BlockageToken = default;
 173
 7174            ClearCoverageTracking();
 7175            UnregisterGridWatcherIfNeeded(keepWatching);
 7176            return;
 177        }
 178
 32179        ObstacleToken blockageToken = BlockageToken;
 32180        BlockageEventInfo removalEventInfo = CreateBlockageEventInfo(blockageToken);
 181
 32182        RemoveAppliedBlockage(blockageToken);
 183
 32184        if (!preserveToken)
 27185            BlockageToken = default;
 32186        IsBlocking = false;
 32187        ClearCoverageTracking();
 32188        UnregisterGridWatcherIfNeeded(keepWatching);
 189
 32190        NotifyBlockageRemoved(removalEventInfo);
 32191    }
 192
 193    private void PrepareBlockageApplication()
 194    {
 162195        CacheMin = GetBoundsMin();
 162196        CacheMax = GetBoundsMax();
 162197        if (!BlockageToken.IsValid)
 154198            BlockageToken = World.AllocateObstacleToken();
 199
 162200        if (CacheCoveredVoxels)
 29201            _cachedCoveredVoxels ??= new SwiftList<WorldVoxelIndex>();
 202
 162203        RegisterGridWatcher();
 162204        ClearCoverageTracking();
 162205    }
 206
 207    private bool ApplyBlockageToCoveredVoxels(ObstacleToken blockageToken, out bool hasCoverage)
 208    {
 162209        hasCoverage = true;
 162210        bool foundCoverage = false;
 162211        SwiftList<WorldVoxelIndex>? appliedVoxels = CacheCoveredVoxels
 162212            ? _cachedCoveredVoxels
 162213            : SwiftListPool<WorldVoxelIndex>.Shared.Rent();
 214
 215        try
 216        {
 642217            foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 218            {
 159219                foundCoverage = true;
 159220                _watchedGridIndices.Add(covered.Grid.GridIndex);
 159221                ApplyBlockageToVoxels(covered, appliedVoxels!, blockageToken, ref hasCoverage);
 222            }
 223
 162224            if (!hasCoverage)
 2225                RollbackAppliedBlockage(appliedVoxels!, blockageToken);
 226
 162227            return foundCoverage;
 228        }
 229        finally
 230        {
 162231            if (!CacheCoveredVoxels)
 133232                SwiftListPool<WorldVoxelIndex>.Shared.Release(appliedVoxels!);
 162233        }
 162234    }
 235
 236    private void ApplyBlockageToVoxels(
 237        GridVoxelSet covered,
 238        SwiftList<WorldVoxelIndex> appliedVoxels,
 239        ObstacleToken blockageToken,
 240        ref bool hasCoverage)
 241    {
 1776242        foreach (Voxel voxel in covered.Voxels)
 243        {
 729244            if (!covered.Grid.TryAddObstacle(voxel, blockageToken))
 245            {
 2246                hasCoverage = false;
 2247                continue;
 248            }
 249
 727250            appliedVoxels.Add(voxel.WorldIndex);
 251        }
 159252    }
 253
 254    private void RollbackAppliedBlockage(
 255        SwiftList<WorldVoxelIndex> appliedVoxels,
 256        ObstacleToken blockageToken)
 257    {
 6258        foreach (WorldVoxelIndex voxelIndex in appliedVoxels)
 1259            GridObstacleManager.TryRemoveObstacle(World, voxelIndex, blockageToken);
 260
 2261        appliedVoxels.Clear();
 2262    }
 263
 264    private void RemoveAppliedBlockage(ObstacleToken blockageToken)
 265    {
 32266        if (CacheCoveredVoxels && _cachedCoveredVoxels?.Count > 0)
 267        {
 17268            RemoveCachedBlockage(blockageToken);
 17269            return;
 270        }
 271
 15272        RemoveTracedBlockage(blockageToken);
 15273    }
 274
 275    private void RemoveCachedBlockage(ObstacleToken blockageToken)
 276    {
 96277        foreach (WorldVoxelIndex voxelIndex in _cachedCoveredVoxels!)
 31278            GridObstacleManager.TryRemoveObstacle(World, voxelIndex, blockageToken);
 17279    }
 280
 281    private void RemoveTracedBlockage(ObstacleToken blockageToken)
 282    {
 62283        foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 284        {
 124285            foreach (Voxel voxel in covered.Voxels)
 46286                covered.Grid.TryRemoveObstacle(voxel, blockageToken);
 287        }
 15288    }
 289
 290    private void ClearCoverageTracking()
 291    {
 201292        _cachedCoveredVoxels?.Clear();
 201293        _watchedGridIndices.Clear();
 201294    }
 295
 296    private void UnregisterGridWatcherIfNeeded(bool keepWatching)
 297    {
 39298        if (!keepWatching || !IsActive)
 29299            UnregisterGridWatcher();
 39300    }
 301
 302    /// <summary>
 303    /// Creates a snapshot describing the current blocker coverage.
 304    /// </summary>
 305    protected BlockageEventInfo CreateBlockageEventInfo()
 306    {
 2307        return CreateBlockageEventInfo(BlockageToken);
 308    }
 309
 310    private BlockageEventInfo CreateBlockageEventInfo(ObstacleToken blockageToken)
 311    {
 34312        return new BlockageEventInfo(World.SpawnToken, blockageToken, CacheMin, CacheMax);
 313    }
 314
 315    /// <summary>
 316    /// Notifies subscribers that blockage has been applied.
 317    /// </summary>
 318    protected virtual void NotifyBlockageApplied()
 319    {
 154320        Action<BlockageEventInfo>? handlers = _onBlockageApplied;
 154321        if (handlers == null)
 152322            return;
 323
 2324        BlockageEventInfo eventInfo = CreateBlockageEventInfo();
 325
 2326        var handlerDelegates = handlers.GetInvocationList();
 10327        for (int i = 0; i < handlerDelegates.Length; i++)
 328        {
 329            try
 330            {
 3331                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 2332            }
 1333            catch (Exception ex)
 334            {
 1335                GridForgeLogger.Channel.Error(
 1336                    $"Blockage apply notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax}
 1337            }
 338        }
 2339    }
 340
 341    /// <summary>
 342    /// Notifies subscribers that blockage has been removed.
 343    /// </summary>
 344    protected virtual void NotifyBlockageRemoved(BlockageEventInfo eventInfo)
 345    {
 32346        Action<BlockageEventInfo>? handlers = _onBlockageRemoved;
 32347        if (handlers == null)
 31348            return;
 349
 1350        var handlerDelegates = handlers.GetInvocationList();
 6351        for (int i = 0; i < handlerDelegates.Length; i++)
 352        {
 353            try
 354            {
 2355                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 1356            }
 1357            catch (Exception ex)
 358            {
 1359                GridForgeLogger.Channel.Error(
 1360                    $"Blockage remove notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax
 1361            }
 362        }
 1363    }
 364
 365    /// <summary>
 366    /// Gets the min bounds of the area to block. Must be implemented by subclasses.
 367    /// </summary>
 368    protected abstract Vector3d GetBoundsMin();
 369
 370    /// <summary>
 371    /// Gets the max bounds of the area to block. Must be implemented by subclasses.
 372    /// </summary>
 373    protected abstract Vector3d GetBoundsMax();
 374
 375    /// <summary>
 376    /// Sets whether or not to cache covered voxels for this blocker.
 377    /// If enabled, the blocker will store references to the voxels it covers when applying blockage,
 378    /// which can improve performance when removing blockage at the cost of increased memory usage.
 379    /// </summary>
 380    public virtual void SetCacheCoveredVoxels(bool cache)
 381    {
 5382        if (CacheCoveredVoxels == cache)
 2383            return;
 384
 3385        CacheCoveredVoxels = cache;
 3386        if (cache)
 387        {
 2388            _cachedCoveredVoxels = new SwiftList<WorldVoxelIndex>();
 2389            return;
 390        }
 391
 1392        _cachedCoveredVoxels = null;
 1393    }
 394
 395    /// <summary>
 396    /// Resets the blocker to its default state, removing any active blockage and clearing cached data.
 397    /// </summary>
 398    public virtual void Reset()
 399    {
 1400        RemoveBlockageCore(keepWatching: false, preserveToken: false);
 401
 1402        CacheCoveredVoxels = false;
 1403        _cachedCoveredVoxels = null;
 1404        _watchedGridIndices.Clear();
 405
 1406        IsActive = false;
 1407        CacheMin = Vector3d.Zero;
 1408        CacheMax = Vector3d.Zero;
 1409        BlockageToken = default;
 1410    }
 411
 412    private void ReapplyBlockage()
 413    {
 11414        if (!IsActive)
 1415            return;
 416
 10417        RemoveBlockageCore(keepWatching: true, preserveToken: true);
 10418        ApplyBlockageCore(preserveTokenWhenEmpty: true);
 10419    }
 420
 421    private void RegisterGridWatcher()
 422    {
 163423        lock (_gridWatcherLock)
 424        {
 163425            if (_isWatchingWorldEvents)
 10426                return;
 427
 153428            World.OnActiveGridAdded += HandleActiveGridAdded;
 153429            World.OnActiveGridRemoved += HandleActiveGridRemoved;
 153430            World.OnActiveGridChange += HandleActiveGridChanged;
 153431            World.OnReset += HandleWorldReset;
 153432            _isWatchingWorldEvents = true;
 153433        }
 163434    }
 435
 436    private void UnregisterGridWatcher()
 437    {
 154438        lock (_gridWatcherLock)
 439        {
 154440            if (!_isWatchingWorldEvents)
 1441                return;
 442
 153443            World.OnActiveGridAdded -= HandleActiveGridAdded;
 153444            World.OnActiveGridRemoved -= HandleActiveGridRemoved;
 153445            World.OnActiveGridChange -= HandleActiveGridChanged;
 153446            World.OnReset -= HandleWorldReset;
 153447            _isWatchingWorldEvents = false;
 153448        }
 154449    }
 450
 451    private bool ShouldReactToGridAdded(GridEventInfo eventInfo)
 452    {
 7453        return IsActive && BoundsOverlap(CacheMin, CacheMax, eventInfo.BoundsMin, eventInfo.BoundsMax);
 454    }
 455
 456    private static bool BoundsOverlap(
 457        Vector3d firstMin,
 458        Vector3d firstMax,
 459        Vector3d secondMin,
 460        Vector3d secondMax)
 461    {
 8462        return AxisOverlaps(firstMin.X, firstMax.X, secondMin.X, secondMax.X)
 8463            && AxisOverlaps(firstMin.Y, firstMax.Y, secondMin.Y, secondMax.Y)
 8464            && AxisOverlaps(firstMin.Z, firstMax.Z, secondMin.Z, secondMax.Z);
 465    }
 466
 467    private static bool AxisOverlaps(Fixed64 firstMin, Fixed64 firstMax, Fixed64 secondMin, Fixed64 secondMax)
 468    {
 22469        return firstMax >= secondMin && firstMin <= secondMax;
 470    }
 471
 472    private bool ShouldReactToGridRemoved(GridEventInfo eventInfo)
 473    {
 5474        return IsActive && _watchedGridIndices.Contains(eventInfo.GridIndex);
 475    }
 476
 477    private bool ShouldReactToGridChanged(GridEventInfo eventInfo)
 478    {
 21009479        return IsActive
 21009480            && IsSparseVoxelMutation(eventInfo.ChangeKind)
 21009481            && BoundsOverlap(CacheMin, CacheMax, eventInfo.AffectedBoundsMin, eventInfo.AffectedBoundsMax);
 482    }
 483
 484    private static bool IsSparseVoxelMutation(GridEventKind changeKind) =>
 21009485        changeKind == GridEventKind.SparseVoxelAdded
 21009486        || changeKind == GridEventKind.SparseVoxelRemoved;
 487
 488    private void HandleActiveGridAdded(GridEventInfo eventInfo)
 489    {
 7490        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridAdded(eventInfo))
 2491            return;
 492
 5493        ReapplyBlockage();
 5494    }
 495
 496    private void HandleActiveGridRemoved(GridEventInfo eventInfo)
 497    {
 5498        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridRemoved(eventInfo))
 2499            return;
 500
 3501        ReapplyBlockage();
 3502    }
 503
 504    private void HandleActiveGridChanged(GridEventInfo eventInfo)
 505    {
 21010506        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridChanged(eventInfo))
 21008507            return;
 508
 2509        ReapplyBlockage();
 2510    }
 511
 512    private void HandleWorldReset()
 513    {
 125514        IsBlocking = false;
 125515        BlockageToken = default;
 125516        _cachedCoveredVoxels?.Clear();
 125517        _watchedGridIndices.Clear();
 125518        UnregisterGridWatcher();
 125519    }
 520}

Methods/Properties

.ctor(GridForge.Grids.GridWorld,System.Boolean,System.Boolean)
add_OnBlockageApplied(System.Action`1<GridForge.Blockers.BlockageEventInfo>)
remove_OnBlockageApplied(System.Action`1<GridForge.Blockers.BlockageEventInfo>)
add_OnBlockageRemoved(System.Action`1<GridForge.Blockers.BlockageEventInfo>)
remove_OnBlockageRemoved(System.Action`1<GridForge.Blockers.BlockageEventInfo>)
ToggleStatus(System.Boolean)
ApplyBlockage()
ApplyBlockageCore(System.Boolean)
RemoveBlockage()
RemoveBlockageCore(System.Boolean,System.Boolean)
PrepareBlockageApplication()
ApplyBlockageToCoveredVoxels(GridForge.ObstacleToken,System.Boolean&)
ApplyBlockageToVoxels(GridForge.GridVoxelSet,SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>,GridForge.ObstacleToken,System.Boolean&)
RollbackAppliedBlockage(SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>,GridForge.ObstacleToken)
RemoveAppliedBlockage(GridForge.ObstacleToken)
RemoveCachedBlockage(GridForge.ObstacleToken)
RemoveTracedBlockage(GridForge.ObstacleToken)
ClearCoverageTracking()
UnregisterGridWatcherIfNeeded(System.Boolean)
CreateBlockageEventInfo()
CreateBlockageEventInfo(GridForge.ObstacleToken)
NotifyBlockageApplied()
NotifyBlockageRemoved(GridForge.Blockers.BlockageEventInfo)
SetCacheCoveredVoxels(System.Boolean)
Reset()
ReapplyBlockage()
RegisterGridWatcher()
UnregisterGridWatcher()
ShouldReactToGridAdded(GridForge.Grids.GridEventInfo)
BoundsOverlap(FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d,FixedMathSharp.Vector3d)
AxisOverlaps(FixedMathSharp.Fixed64,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64,FixedMathSharp.Fixed64)
ShouldReactToGridRemoved(GridForge.Grids.GridEventInfo)
ShouldReactToGridChanged(GridForge.Grids.GridEventInfo)
IsSparseVoxelMutation(GridForge.Grids.GridEventKind)
HandleActiveGridAdded(GridForge.Grids.GridEventInfo)
HandleActiveGridRemoved(GridForge.Grids.GridEventInfo)
HandleActiveGridChanged(GridForge.Grids.GridEventInfo)
HandleWorldReset()