< Summary

Information
Class: GridForge.Blockers.Blocker
Assembly: GridForge
File(s): /home/runner/work/GridForge/GridForge/src/GridForge/Blockers/Blocker.cs
Line coverage
99%
Covered lines: 166
Uncovered lines: 1
Coverable lines: 167
Total lines: 402
Line coverage: 99.4%
Branch coverage
98%
Covered branches: 106
Total branches: 108
Branch coverage: 98.1%
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
 1using FixedMathSharp;
 2using GridForge.Grids;
 3using GridForge.Spatial;
 4using GridForge.Utility;
 5using SwiftCollections;
 6using System;
 7
 8namespace GridForge.Blockers;
 9
 10/// <summary>
 11/// Base class for all grid blockers that handles applying and removing obstacles.
 12/// </summary>
 13public abstract class Blocker : IBlocker
 14{
 13515    private readonly object _gridWatcherLock = new();
 16    private bool _isWatchingWorldEvents;
 17
 18    /// <summary>
 19    /// The world this blocker is bound to.
 20    /// </summary>
 112321    public GridWorld World { get; }
 22
 23    /// <summary>
 24    /// Unique token representing this blockage instance.
 25    /// </summary>
 98626    public BoundsKey BlockageToken { get; protected set; } = default;
 27
 28    /// <summary>
 29    /// Indicates whether the blocker is currently active.
 30    /// </summary>
 29731    public bool IsActive { get; protected set; }
 32
 33    /// <summary>
 34    /// The cached minimum bounds of the blockage area.
 35    /// </summary>
 44536    public Vector3d CacheMin { get; protected set; }
 37
 38    /// <summary>
 39    /// The cached maximum bounds of the blockage area.
 40    /// </summary>
 44641    public Vector3d CacheMax { get; private set; }
 42
 43    /// <summary>
 44    /// Tracks whether the blocker is currently blocking voxels.
 45    /// </summary>
 60146    public bool IsBlocking { get; protected set; }
 47
 48    /// <summary>
 49    /// Flags whether or not to hold onto a reference of the voxels this blocker covers.
 50    /// </summary>
 95351    public bool CacheCoveredVoxels { get; protected set; }
 52
 53    /// <summary>
 54    /// Stable voxel identifiers cached for safe blocker removal when <see cref="CacheCoveredVoxels"/> is true.
 55    /// </summary>
 56    protected SwiftList<WorldVoxelIndex>? _cachedCoveredVoxels;
 57
 58    /// <summary>
 59    /// Grid indices currently covered by this blocker.
 60    /// </summary>
 13561    private readonly SwiftHashSet<ushort> _watchedGridIndices = new();
 62
 63    /// <summary>
 64    /// Event triggered when a blocker is applied.
 65    /// </summary>
 66    private static Action<BlockageEventInfo>? _onBlockageApplied;
 67
 68    /// <inheritdoc cref="_onBlockageApplied"/>
 69    public static event Action<BlockageEventInfo> OnBlockageApplied
 70    {
 271        add => _onBlockageApplied += value;
 272        remove => _onBlockageApplied -= value;
 73    }
 74
 75    /// <summary>
 76    /// Event triggered when a blocker is removed.
 77    /// </summary>
 78    private static Action<BlockageEventInfo>? _onBlockageRemoved;
 79
 80    /// <inheritdoc cref="_onBlockageRemoved"/>
 81    public static event Action<BlockageEventInfo> OnBlockageRemoved
 82    {
 283        add => _onBlockageRemoved += value;
 284        remove => _onBlockageRemoved -= value;
 85    }
 86
 87    /// <summary>
 88    /// Initializes a new blocker instance bound to the supplied world.
 89    /// </summary>
 90    /// <param name="world">The world whose grids this blocker should affect.</param>
 91    /// <param name="active">Flag whether or not this blocker will block on update.</param>
 92    /// <param name="cacheCoveredVoxels">Flag whether or not to cache covered voxels that are blocked.</param>
 13593    protected Blocker(GridWorld world, bool active = true, bool cacheCoveredVoxels = false)
 94    {
 13595        World = world ?? throw new ArgumentNullException(nameof(world));
 13596        IsActive = active;
 13597        CacheCoveredVoxels = cacheCoveredVoxels;
 13598    }
 99
 100    /// <summary>
 101    /// Toggles the blocker from inactive to active or active to inactive state
 102    /// If object is currently blocking, the blocker will be removed.
 103    /// If object is not active and not blocking, the blocker will be applied.
 104    /// </summary>
 105    public virtual void ToggleStatus(bool status)
 106    {
 2107        if (!status)
 108        {
 1109            RemoveBlockageCore(keepWatching: false);
 1110            IsActive = false;
 1111            return;
 112        }
 113
 1114        if (!IsBlocking)
 115        {
 1116            IsActive = true;
 1117            ApplyBlockage();
 118        }
 1119    }
 120
 121    /// <summary>
 122    /// Applies the blockage by marking voxels as obstacles.
 123    /// </summary>
 124    public virtual void ApplyBlockage()
 125    {
 140126        if (!IsActive || IsBlocking || !World.IsActive)
 2127            return;
 128
 138129        CacheMin = GetBoundsMin();
 138130        CacheMax = GetBoundsMax();
 138131        BlockageToken = new BoundsKey(CacheMin, CacheMax);
 132
 138133        if (CacheCoveredVoxels)
 10134            _cachedCoveredVoxels ??= new SwiftList<WorldVoxelIndex>();
 135
 138136        RegisterGridWatcher();
 138137        _cachedCoveredVoxels?.Clear();
 138138        _watchedGridIndices.Clear();
 139
 138140        bool hasCoverage = true;
 138141        bool foundCoverage = false;
 552142        foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 143        {
 138144            if (covered.Voxels.Count <= 0)
 145                continue;
 146
 137147            foundCoverage = true;
 137148            _watchedGridIndices.Add(covered.Grid.GridIndex);
 149
 1594150            foreach (Voxel voxel in covered.Voxels)
 151            {
 660152                if (!covered.Grid.TryAddObstacle(voxel, BlockageToken))
 153                {
 1154                    hasCoverage = false;
 1155                    continue;
 156                }
 157
 659158                if (CacheCoveredVoxels)
 16159                    _cachedCoveredVoxels!.Add(voxel.WorldIndex);
 160            }
 161        }
 162
 138163        IsBlocking = foundCoverage && hasCoverage;
 164
 138165        if (IsBlocking)
 166        {
 134167            NotifyBlockageApplied();
 134168            return;
 169        }
 170
 4171        if (!foundCoverage)
 3172            BlockageToken = default;
 4173    }
 174
 175    /// <summary>
 176    /// Removes the blockage by clearing obstacle markers from voxels.
 177    /// </summary>
 178    public virtual void RemoveBlockage()
 179    {
 8180        RemoveBlockageCore(keepWatching: false);
 8181    }
 182
 183    private void RemoveBlockageCore(bool keepWatching)
 184    {
 14185        if (!IsBlocking)
 186        {
 3187            _cachedCoveredVoxels?.Clear();
 3188            _watchedGridIndices.Clear();
 3189            if (!keepWatching || !IsActive)
 1190                UnregisterGridWatcher();
 3191            return;
 192        }
 193
 11194        BlockageEventInfo removalEventInfo = CreateBlockageEventInfo();
 195
 11196        if (CacheCoveredVoxels && _cachedCoveredVoxels?.Count > 0)
 197        {
 24198            foreach (WorldVoxelIndex voxelIndex in _cachedCoveredVoxels)
 10199                GridObstacleManager.TryRemoveObstacle(World, voxelIndex, BlockageToken);
 200        }
 201        else
 202        {
 38203            foreach (GridVoxelSet covered in GridTracer.GetCoveredVoxels(World, CacheMin, CacheMax))
 204            {
 68205                foreach (Voxel voxel in covered.Voxels)
 24206                    covered.Grid.TryRemoveObstacle(voxel, BlockageToken);
 207            }
 208        }
 209
 11210        BlockageToken = default;
 11211        IsBlocking = false;
 11212        _cachedCoveredVoxels?.Clear();
 11213        _watchedGridIndices.Clear();
 214
 11215        if (!keepWatching || !IsActive)
 9216            UnregisterGridWatcher();
 217
 11218        NotifyBlockageRemoved(removalEventInfo);
 11219    }
 220
 221    /// <summary>
 222    /// Creates a snapshot describing the current blocker coverage.
 223    /// </summary>
 224    protected BlockageEventInfo CreateBlockageEventInfo()
 225    {
 12226        return new BlockageEventInfo(World.SpawnToken, BlockageToken, CacheMin, CacheMax);
 227    }
 228
 229    /// <summary>
 230    /// Notifies subscribers that blockage has been applied.
 231    /// </summary>
 232    protected virtual void NotifyBlockageApplied()
 233    {
 134234        Action<BlockageEventInfo>? handlers = _onBlockageApplied;
 134235        if (handlers == null)
 133236            return;
 237
 1238        BlockageEventInfo eventInfo = CreateBlockageEventInfo();
 239
 1240        var handlerDelegates = handlers.GetInvocationList();
 6241        for (int i = 0; i < handlerDelegates.Length; i++)
 242        {
 243            try
 244            {
 2245                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 1246            }
 1247            catch (Exception ex)
 248            {
 1249                GridForgeLogger.Channel.Error(
 1250                    $"Blockage apply notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax}
 1251            }
 252        }
 1253    }
 254
 255    /// <summary>
 256    /// Notifies subscribers that blockage has been removed.
 257    /// </summary>
 258    protected virtual void NotifyBlockageRemoved(BlockageEventInfo eventInfo)
 259    {
 11260        Action<BlockageEventInfo>? handlers = _onBlockageRemoved;
 11261        if (handlers == null)
 10262            return;
 263
 1264        var handlerDelegates = handlers.GetInvocationList();
 6265        for (int i = 0; i < handlerDelegates.Length; i++)
 266        {
 267            try
 268            {
 2269                ((Action<BlockageEventInfo>)handlerDelegates[i])(eventInfo);
 1270            }
 1271            catch (Exception ex)
 272            {
 1273                GridForgeLogger.Channel.Error(
 1274                    $"Blockage remove notification: {ex.Message} | Bounds: {eventInfo.BoundsMin} -> {eventInfo.BoundsMax
 1275            }
 276        }
 1277    }
 278
 279    /// <summary>
 280    /// Gets the min bounds of the area to block. Must be implemented by subclasses.
 281    /// </summary>
 282    protected abstract Vector3d GetBoundsMin();
 283
 284    /// <summary>
 285    /// Gets the max bounds of the area to block. Must be implemented by subclasses.
 286    /// </summary>
 287    protected abstract Vector3d GetBoundsMax();
 288
 289    /// <summary>
 290    /// Sets whether or not to cache covered voxels for this blocker.
 291    /// If enabled, the blocker will store references to the voxels it covers when applying blockage,
 292    /// which can improve performance when removing blockage at the cost of increased memory usage.
 293    /// </summary>
 294    public virtual void SetCacheCoveredVoxels(bool cache)
 295    {
 4296        if (CacheCoveredVoxels == cache)
 1297            return;
 298
 3299        CacheCoveredVoxels = cache;
 3300        if (cache && _cachedCoveredVoxels == null)
 2301            _cachedCoveredVoxels = new SwiftList<WorldVoxelIndex>();
 1302        else if (!cache)
 1303            _cachedCoveredVoxels = null;
 1304    }
 305
 306    /// <summary>
 307    /// Resets the blocker to its default state, removing any active blockage and clearing cached data.
 308    /// </summary>
 309    public virtual void Reset()
 310    {
 1311        RemoveBlockageCore(keepWatching: false);
 312
 1313        CacheCoveredVoxels = false;
 1314        _cachedCoveredVoxels = null;
 1315        _watchedGridIndices.Clear();
 316
 1317        IsActive = false;
 1318        CacheMin = Vector3d.Zero;
 1319        CacheMax = Vector3d.Zero;
 1320        BlockageToken = default;
 1321    }
 322
 323    private void ReapplyBlockage()
 324    {
 5325        if (!IsActive)
 1326            return;
 327
 4328        RemoveBlockageCore(keepWatching: true);
 4329        ApplyBlockage();
 4330    }
 331
 332    private void RegisterGridWatcher()
 333    {
 139334        lock (_gridWatcherLock)
 335        {
 139336            if (_isWatchingWorldEvents)
 4337                return;
 338
 135339            World.OnActiveGridAdded += HandleActiveGridAdded;
 135340            World.OnActiveGridRemoved += HandleActiveGridRemoved;
 135341            World.OnReset += HandleWorldReset;
 135342            _isWatchingWorldEvents = true;
 135343        }
 139344    }
 345
 346    private void UnregisterGridWatcher()
 347    {
 135348        lock (_gridWatcherLock)
 349        {
 135350            if (!_isWatchingWorldEvents)
 0351                return;
 352
 135353            World.OnActiveGridAdded -= HandleActiveGridAdded;
 135354            World.OnActiveGridRemoved -= HandleActiveGridRemoved;
 135355            World.OnReset -= HandleWorldReset;
 135356            _isWatchingWorldEvents = false;
 135357        }
 135358    }
 359
 360    private bool ShouldReactToGridAdded(GridEventInfo eventInfo)
 361    {
 5362        if (!IsActive)
 1363            return false;
 364
 4365        return CacheMax.x >= eventInfo.BoundsMin.x
 4366            && CacheMin.x <= eventInfo.BoundsMax.x
 4367            && CacheMax.y >= eventInfo.BoundsMin.y
 4368            && CacheMin.y <= eventInfo.BoundsMax.y
 4369            && CacheMax.z >= eventInfo.BoundsMin.z
 4370            && CacheMin.z <= eventInfo.BoundsMax.z;
 371    }
 372
 373    private bool ShouldReactToGridRemoved(GridEventInfo eventInfo)
 374    {
 3375        return IsActive && _watchedGridIndices.Contains(eventInfo.GridIndex);
 376    }
 377
 378    private void HandleActiveGridAdded(GridEventInfo eventInfo)
 379    {
 4380        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridAdded(eventInfo))
 1381            return;
 382
 3383        ReapplyBlockage();
 3384    }
 385
 386    private void HandleActiveGridRemoved(GridEventInfo eventInfo)
 387    {
 2388        if (eventInfo.WorldSpawnToken != World.SpawnToken || !ShouldReactToGridRemoved(eventInfo))
 1389            return;
 390
 1391        ReapplyBlockage();
 1392    }
 393
 394    private void HandleWorldReset()
 395    {
 125396        IsBlocking = false;
 125397        BlockageToken = default;
 125398        _cachedCoveredVoxels?.Clear();
 125399        _watchedGridIndices.Clear();
 125400        UnregisterGridWatcher();
 125401    }
 402}