< 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: 193
Uncovered lines: 0
Coverable lines: 193
Total lines: 499
Line coverage: 100%
Branch coverage
99%
Covered branches: 101
Total branches: 102
Branch coverage: 99%
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 FixedMathSharp;
 9using GridForge.Grids;
 10using GridForge.Spatial;
 11using GridForge.Utility;
 12using SwiftCollections;
 13using SwiftCollections.Pool;
 14using System;
 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{
 14823    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 BoundsKey BlockageToken { get; protected set; } = default;
 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>
 14869    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    {
 391        add => _onBlockageRemoved += value;
 392        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>
 148101    protected Blocker(GridWorld world, bool active = true, bool cacheCoveredVoxels = false)
 102    {
 148103        SwiftThrowHelper.ThrowIfNull(world, nameof(world));
 148104        World = world;
 148105        IsActive = active;
 148106        CacheCoveredVoxels = cacheCoveredVoxels;
 148107    }
 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);
 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    {
 156135        if (!IsActive || IsBlocking || !World.IsActive)
 2136            return;
 137
 154138        PrepareBlockageApplication();
 154139        bool foundCoverage = ApplyBlockageToCoveredVoxels(out bool hasCoverage);
 140
 154141        IsBlocking = foundCoverage && hasCoverage;
 142
 154143        if (IsBlocking)
 144        {
 147145            NotifyBlockageApplied();
 147146            return;
 147        }
 148
 7149        BlockageToken = default;
 7150    }
 151
 152    /// <summary>
 153    /// Removes the blockage by clearing obstacle markers from voxels.
 154    /// </summary>
 155    public virtual void RemoveBlockage()
 156    {
 20157        RemoveBlockageCore(keepWatching: false);
 20158    }
 159
 160    private void RemoveBlockageCore(bool keepWatching)
 161    {
 30162        if (!IsBlocking)
 163        {
 6164            ClearCoverageTracking();
 6165            UnregisterGridWatcherIfNeeded(keepWatching);
 6166            return;
 167        }
 168
 24169        BlockageEventInfo removalEventInfo = CreateBlockageEventInfo();
 170
 24171        RemoveAppliedBlockage();
 172
 24173        BlockageToken = default;
 24174        IsBlocking = false;
 24175        ClearCoverageTracking();
 24176        UnregisterGridWatcherIfNeeded(keepWatching);
 177
 24178        NotifyBlockageRemoved(removalEventInfo);
 24179    }
 180
 181    private void PrepareBlockageApplication()
 182    {
 154183        CacheMin = GetBoundsMin();
 154184        CacheMax = GetBoundsMax();
 154185        BlockageToken = new BoundsKey(CacheMin, CacheMax);
 186
 154187        if (CacheCoveredVoxels)
 22188            _cachedCoveredVoxels ??= new SwiftList<WorldVoxelIndex>();
 189
 154190        RegisterGridWatcher();
 154191        ClearCoverageTracking();
 154192    }
 193
 194    private bool ApplyBlockageToCoveredVoxels(out bool hasCoverage)
 195    {
 154196        hasCoverage = true;
 154197        bool foundCoverage = false;
 154198        SwiftList<WorldVoxelIndex>? appliedVoxels = CacheCoveredVoxels
 154199            ? _cachedCoveredVoxels
 154200            : SwiftListPool<WorldVoxelIndex>.Shared.Rent();
 201
 202        try
 203        {
 614204            foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 205            {
 153206                foundCoverage = true;
 153207                _watchedGridIndices.Add(covered.Grid.GridIndex);
 153208                ApplyBlockageToVoxels(covered, appliedVoxels!, ref hasCoverage);
 209            }
 210
 154211            if (!hasCoverage)
 2212                RollbackAppliedBlockage(appliedVoxels!);
 213
 154214            return foundCoverage;
 215        }
 216        finally
 217        {
 154218            if (!CacheCoveredVoxels)
 132219                SwiftListPool<WorldVoxelIndex>.Shared.Release(appliedVoxels!);
 154220        }
 154221    }
 222
 223    private void ApplyBlockageToVoxels(
 224        GridVoxelSet covered,
 225        SwiftList<WorldVoxelIndex> appliedVoxels,
 226        ref bool hasCoverage)
 227    {
 1750228        foreach (Voxel voxel in covered.Voxels)
 229        {
 722230            if (!covered.Grid.TryAddObstacle(voxel, BlockageToken))
 231            {
 2232                hasCoverage = false;
 2233                continue;
 234            }
 235
 720236            appliedVoxels.Add(voxel.WorldIndex);
 237        }
 153238    }
 239
 240    private void RollbackAppliedBlockage(SwiftList<WorldVoxelIndex> appliedVoxels)
 241    {
 6242        foreach (WorldVoxelIndex voxelIndex in appliedVoxels)
 1243            GridObstacleManager.TryRemoveObstacle(World, voxelIndex, BlockageToken);
 244
 2245        appliedVoxels.Clear();
 2246    }
 247
 248    private void RemoveAppliedBlockage()
 249    {
 24250        if (CacheCoveredVoxels && _cachedCoveredVoxels?.Count > 0)
 251        {
 11252            RemoveCachedBlockage();
 11253            return;
 254        }
 255
 13256        RemoveTracedBlockage();
 13257    }
 258
 259    private void RemoveCachedBlockage()
 260    {
 86261        foreach (WorldVoxelIndex voxelIndex in _cachedCoveredVoxels!)
 32262            GridObstacleManager.TryRemoveObstacle(World, voxelIndex, BlockageToken);
 11263    }
 264
 265    private void RemoveTracedBlockage()
 266    {
 54267        foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 268        {
 86269            foreach (Voxel voxel in covered.Voxels)
 29270                covered.Grid.TryRemoveObstacle(voxel, BlockageToken);
 271        }
 13272    }
 273
 274    private void ClearCoverageTracking()
 275    {
 184276        _cachedCoveredVoxels?.Clear();
 184277        _watchedGridIndices.Clear();
 184278    }
 279
 280    private void UnregisterGridWatcherIfNeeded(bool keepWatching)
 281    {
 30282        if (!keepWatching || !IsActive)
 22283            UnregisterGridWatcher();
 30284    }
 285
 286    /// <summary>
 287    /// Creates a snapshot describing the current blocker coverage.
 288    /// </summary>
 289    protected BlockageEventInfo CreateBlockageEventInfo()
 290    {
 27291        return new BlockageEventInfo(World.SpawnToken, BlockageToken, CacheMin, CacheMax);
 292    }
 293
 294    /// <summary>
 295    /// Notifies subscribers that blockage has been applied.
 296    /// </summary>
 297    protected virtual void NotifyBlockageApplied()
 298    {
 147299        Action<BlockageEventInfo>? handlers = _onBlockageApplied;
 147300        if (handlers == null)
 144301            return;
 302
 3303        BlockageEventInfo eventInfo = CreateBlockageEventInfo();
 304
 3305        var handlerDelegates = handlers.GetInvocationList();
 14306        for (int i = 0; i < handlerDelegates.Length; i++)
 307        {
 308            try
 309            {
 4310                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 3311            }
 1312            catch (Exception ex)
 313            {
 1314                GridForgeLogger.Channel.Error(
 1315                    $"Blockage apply notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax}
 1316            }
 317        }
 3318    }
 319
 320    /// <summary>
 321    /// Notifies subscribers that blockage has been removed.
 322    /// </summary>
 323    protected virtual void NotifyBlockageRemoved(BlockageEventInfo eventInfo)
 324    {
 24325        Action<BlockageEventInfo>? handlers = _onBlockageRemoved;
 24326        if (handlers == null)
 21327            return;
 328
 3329        var handlerDelegates = handlers.GetInvocationList();
 14330        for (int i = 0; i < handlerDelegates.Length; i++)
 331        {
 332            try
 333            {
 4334                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 3335            }
 1336            catch (Exception ex)
 337            {
 1338                GridForgeLogger.Channel.Error(
 1339                    $"Blockage remove notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax
 1340            }
 341        }
 3342    }
 343
 344    /// <summary>
 345    /// Gets the min bounds of the area to block. Must be implemented by subclasses.
 346    /// </summary>
 347    protected abstract Vector3d GetBoundsMin();
 348
 349    /// <summary>
 350    /// Gets the max bounds of the area to block. Must be implemented by subclasses.
 351    /// </summary>
 352    protected abstract Vector3d GetBoundsMax();
 353
 354    /// <summary>
 355    /// Sets whether or not to cache covered voxels for this blocker.
 356    /// If enabled, the blocker will store references to the voxels it covers when applying blockage,
 357    /// which can improve performance when removing blockage at the cost of increased memory usage.
 358    /// </summary>
 359    public virtual void SetCacheCoveredVoxels(bool cache)
 360    {
 5361        if (CacheCoveredVoxels == cache)
 2362            return;
 363
 3364        CacheCoveredVoxels = cache;
 3365        if (cache)
 366        {
 2367            _cachedCoveredVoxels = new SwiftList<WorldVoxelIndex>();
 2368            return;
 369        }
 370
 1371        _cachedCoveredVoxels = null;
 1372    }
 373
 374    /// <summary>
 375    /// Resets the blocker to its default state, removing any active blockage and clearing cached data.
 376    /// </summary>
 377    public virtual void Reset()
 378    {
 1379        RemoveBlockageCore(keepWatching: false);
 380
 1381        CacheCoveredVoxels = false;
 1382        _cachedCoveredVoxels = null;
 1383        _watchedGridIndices.Clear();
 384
 1385        IsActive = false;
 1386        CacheMin = Vector3d.Zero;
 1387        CacheMax = Vector3d.Zero;
 1388        BlockageToken = default;
 1389    }
 390
 391    private void ReapplyBlockage()
 392    {
 9393        if (!IsActive)
 1394            return;
 395
 8396        RemoveBlockageCore(keepWatching: true);
 8397        ApplyBlockage();
 8398    }
 399
 400    private void RegisterGridWatcher()
 401    {
 155402        lock (_gridWatcherLock)
 403        {
 155404            if (_isWatchingWorldEvents)
 8405                return;
 406
 147407            World.OnActiveGridAdded += HandleActiveGridAdded;
 147408            World.OnActiveGridRemoved += HandleActiveGridRemoved;
 147409            World.OnActiveGridChange += HandleActiveGridChanged;
 147410            World.OnReset += HandleWorldReset;
 147411            _isWatchingWorldEvents = true;
 147412        }
 155413    }
 414
 415    private void UnregisterGridWatcher()
 416    {
 148417        lock (_gridWatcherLock)
 418        {
 148419            if (!_isWatchingWorldEvents)
 1420                return;
 421
 147422            World.OnActiveGridAdded -= HandleActiveGridAdded;
 147423            World.OnActiveGridRemoved -= HandleActiveGridRemoved;
 147424            World.OnActiveGridChange -= HandleActiveGridChanged;
 147425            World.OnReset -= HandleWorldReset;
 147426            _isWatchingWorldEvents = false;
 147427        }
 148428    }
 429
 430    private bool ShouldReactToGridAdded(GridEventInfo eventInfo)
 431    {
 6432        return IsActive && BoundsOverlap(CacheMin, CacheMax, eventInfo.BoundsMin, eventInfo.BoundsMax);
 433    }
 434
 435    private static bool BoundsOverlap(
 436        Vector3d firstMin,
 437        Vector3d firstMax,
 438        Vector3d secondMin,
 439        Vector3d secondMax)
 440    {
 7441        return AxisOverlaps(firstMin.X, firstMax.X, secondMin.X, secondMax.X)
 7442            && AxisOverlaps(firstMin.Y, firstMax.Y, secondMin.Y, secondMax.Y)
 7443            && AxisOverlaps(firstMin.Z, firstMax.Z, secondMin.Z, secondMax.Z);
 444    }
 445
 446    private static bool AxisOverlaps(Fixed64 firstMin, Fixed64 firstMax, Fixed64 secondMin, Fixed64 secondMax)
 447    {
 19448        return firstMax >= secondMin && firstMin <= secondMax;
 449    }
 450
 451    private bool ShouldReactToGridRemoved(GridEventInfo eventInfo)
 452    {
 4453        return IsActive && _watchedGridIndices.Contains(eventInfo.GridIndex);
 454    }
 455
 456    private bool ShouldReactToGridChanged(GridEventInfo eventInfo)
 457    {
 21285458        return IsActive
 21285459            && IsSparseVoxelMutation(eventInfo.ChangeKind)
 21285460            && BoundsOverlap(CacheMin, CacheMax, eventInfo.AffectedBoundsMin, eventInfo.AffectedBoundsMax);
 461    }
 462
 463    private static bool IsSparseVoxelMutation(GridEventKind changeKind) =>
 21285464        changeKind == GridEventKind.SparseVoxelAdded
 21285465        || changeKind == GridEventKind.SparseVoxelRemoved;
 466
 467    private void HandleActiveGridAdded(GridEventInfo eventInfo)
 468    {
 6469        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridAdded(eventInfo))
 2470            return;
 471
 4472        ReapplyBlockage();
 4473    }
 474
 475    private void HandleActiveGridRemoved(GridEventInfo eventInfo)
 476    {
 4477        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridRemoved(eventInfo))
 2478            return;
 479
 2480        ReapplyBlockage();
 2481    }
 482
 483    private void HandleActiveGridChanged(GridEventInfo eventInfo)
 484    {
 21286485        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridChanged(eventInfo))
 21284486            return;
 487
 2488        ReapplyBlockage();
 2489    }
 490
 491    private void HandleWorldReset()
 492    {
 126493        IsBlocking = false;
 126494        BlockageToken = default;
 126495        _cachedCoveredVoxels?.Clear();
 126496        _watchedGridIndices.Clear();
 126497        UnregisterGridWatcher();
 126498    }
 499}

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()
RemoveBlockage()
RemoveBlockageCore(System.Boolean)
PrepareBlockageApplication()
ApplyBlockageToCoveredVoxels(System.Boolean&)
ApplyBlockageToVoxels(GridForge.GridVoxelSet,SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>,System.Boolean&)
RollbackAppliedBlockage(SwiftCollections.SwiftList`1<GridForge.Spatial.WorldVoxelIndex>)
RemoveAppliedBlockage()
RemoveCachedBlockage()
RemoveTracedBlockage()
ClearCoverageTracking()
UnregisterGridWatcherIfNeeded(System.Boolean)
CreateBlockageEventInfo()
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()