Grid/Grid.cs

Grid class for A* pathfinding. Manages grid settings, cell storage and queries, cell generation and tagging, line-of-sight and walkability checks, jump/drop connection assignment, and threaded generation tasks against a Scene.

NetworkingFile Access
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Sandbox;

namespace GridAStar;

public partial class Grid : IValid
{
	public static Grid Main
	{
		get => Grids.GetValueOrDefault( "main" );
		set
		{
			if ( Grids.ContainsKey( "main" ) )
				Grids["main"] = value;
			else
				Grids.Add( "main", value );
		}
	}

	public static Dictionary<string, Grid> Grids { get; set; } = new();

	/// <summary>The scene this grid traces against. Set on creation (Scene-System port).</summary>
	public Scene Scene { get; set; }

	// --- Generation diagnostics ---
	public static int DebugCastsHit;
	public static int DebugAngleRejected;
	public static int DebugOutOfBounds;

	public GridBuilder Settings { get; internal set; }
	public string Identifier => Settings.Identifier;
	public Dictionary<IntVector2, List<Cell>> CellStacks { get; internal set; } = new();
	public IEnumerable<Cell> AllCells => CellStacks.Values.SelectMany( list => list );
	public Vector3 Position => Settings.Position;
	public BBox Bounds => Settings.Bounds;
	public BBox RotatedBounds => Bounds.GetRotatedBounds( Rotation );
	public BBox WorldBounds => RotatedBounds.Translate( Position );
	public Transform Transform => new Transform( WorldBounds.Center, AxisRotation );
	public Rotation Rotation => Settings.Rotation;
	public bool AxisAligned => Settings.AxisAligned;
	public float StandableAngle => Settings.StandableAngle;
	public float StepSize => Settings.StepSize;
	public float CellSize => Settings.CellSize;
	public float HeightClearance => Settings.HeightClearance;
	public float WidthClearance => Settings.WidthClearance;
	public bool GridPerfect => Settings.GridPerfect;
	public bool StaticOnly => Settings.StaticOnly;
	public float MaxDropHeight => Settings.MaxDropHeight;
	public List<JumpDefinition> JumpDefinitions => Settings.JumpDefinitions;
	public int MinNeighbourCount => Settings.MinNeighbourCount;
	public bool IgnoreConnectionsForJumps => Settings.IgnoreConnectionsForJumps;
	public bool IgnoreLOSForJumps => Settings.IgnoreLOSForJumps;
	public bool CylinderShaped => Settings.CylinderShaped;
	public float Tolerance => GridPerfect ? 0.001f : 0f;
	public Rotation AxisRotation => AxisAligned ? new Rotation() : Rotation;
	public int MinimumColumn => WorldBounds.Mins.ToIntVector2( CellSize ).y;
	public int MaximumColumn => WorldBounds.Maxs.ToIntVector2( CellSize ).y;
	public int Columns => MaximumColumn - MinimumColumn;
	public int MinimumRow => WorldBounds.Mins.ToIntVector2( CellSize ).x;
	public int MaximumRow => WorldBounds.Maxs.ToIntVector2( CellSize ).x;
	public int Rows => MaximumRow - MinimumRow;
	bool IValid.IsValid { get; }

	public Grid()
	{
		Settings = new GridBuilder();
	}

	public Grid( GridBuilder settings )
	{
		Settings = settings;
	}

	public void Print( string message ) => Print( Identifier, message );
	public static void Print( string identifier, string message ) => Log.Info( $"Grid '{identifier}': {message}" );

	public BBox ToWorld( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation ).Translate( WorldBounds.Center );
	public BBox ToLocal( BBox bounds ) => bounds.GetRotatedBounds( AxisRotation.Inverse ).Translate( -WorldBounds.Center );

	public IntVector2 PositionToCoordinates( Vector3 position ) => (position - WorldBounds.Mins - CellSize / 2).ToIntVector2( CellSize );

	/// <summary>Find the nearest cell from a position even if outside the grid (expensive).</summary>
	public Cell GetNearestCell( Vector3 position, bool onlyBelow = true, bool unoccupiedOnly = false )
	{
		var validCells = AllCells;

		if ( unoccupiedOnly )
			validCells = validCells.Where( x => !x.Occupied );
		if ( onlyBelow )
			validCells = validCells.Where( x => x.Vertices.Min() - Math.Max( HeightClearance, StepSize ) <= position.z );

		return validCells.OrderBy( x => x.Position.DistanceSquared( position ) )
			.FirstOrDefault();
	}

	public Cell GetCellInArea( Vector3 position, float width, bool onlyBelow = true, bool withinStepRange = true )
	{
		var cellsToCheck = (int)Math.Ceiling( width / CellSize ) * 2;
		for ( int y = 0; y <= cellsToCheck; y++ )
		{
			var spiralY = MathAStar.SpiralPattern( y );
			for ( int x = 0; x <= cellsToCheck; x++ )
			{
				var spiralX = MathAStar.SpiralPattern( x );
				var cellFound = GetCell( position + AxisRotation.Forward * spiralX * CellSize + AxisRotation.Right * spiralY * CellSize + Vector3.Up * StepSize, onlyBelow );

				if ( cellFound == null ) continue;

				if ( withinStepRange )
					if ( position.z - cellFound.Position.z <= Math.Max( HeightClearance, StepSize ) ) return cellFound; else continue;

				return cellFound;
			}
		}

		return null;
	}

	public Cell GetCell( Vector3 position, bool onlyBelow = true ) => GetCell( PositionToCoordinates( position ), onlyBelow ? position.z : WorldBounds.Maxs.z );

	public Cell GetCell( IntVector2 coordinates, float height )
	{
		var cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinates );

		if ( cellsAtCoordinates == null ) return null;

		// Return the cell CLOSEST to the query height among the candidates, not the first match. A column on
		// a spiral staircase / multi-floor area stacks several cells at the same XY; the original first-match
		// returned an arbitrary one (often the bottom of the spiral), so an NPC partway up got a path from the
		// wrong height and couldn't follow it. Candidate window (<= height + clearance) is unchanged.
		Cell best = null;
		var bestDist = float.MaxValue;

		foreach ( var cell in cellsAtCoordinates )
		{
			if ( cell.Vertices.Min() - Math.Max( HeightClearance, StepSize ) >= height )
				continue;

			var dist = Math.Abs( cell.Position.z - height );
			if ( dist < bestDist )
			{
				bestDist = dist;
				best = cell;
			}
		}

		return best;
	}

	public void AddCell( Cell cell )
	{
		if ( cell == null ) return;
		var coordinates = cell.GridPosition;
		if ( !CellStacks.ContainsKey( coordinates ) )
			CellStacks.Add( coordinates, new List<Cell>() { cell } );
		else
			if ( !CellStacks[coordinates].Any( x => Math.Abs( x.Position.z - cell.Position.z ) < Math.Max( HeightClearance, StepSize ) ) )
			CellStacks[coordinates].Add( cell );
	}

	public Cell GetCellInDirection( Cell startingCell, Vector3 direction, int numOfCellsInDirection = 1 ) => GetCell( startingCell.Position + direction * CellSize * numOfCellsInDirection );

	public Cell GetNeighbourInDirection( Cell cell, Vector3 direction )
	{
		var horizontalDirection = direction.WithZ( 0 ).Normal;
		var localCoordinates = horizontalDirection.ToIntVector2();
		var coordinatesToCheck = cell.GridPosition + localCoordinates;

		var cellsAtCoordinates = CellStacks.GetValueOrDefault( coordinatesToCheck );

		if ( cellsAtCoordinates == null ) return null;

		foreach ( var cellAtCoordinate in cellsAtCoordinates )
			if ( cell.IsNeighbour( cellAtCoordinate ) && cell != cellAtCoordinate )
				return cellAtCoordinate;

		return null;
	}

	/// <summary>Returns if there's a valid, unoccupied, and direct line of sight from a cell to another</summary>
	public bool LineOfSight( Cell startingCell, Cell endingCell, Component pathCreator = null, bool debugShow = false )
	{
		var startingPosition = startingCell.Position;
		var endingPosition = endingCell.Position;
		var distanceInSteps = (int)Math.Ceiling( startingPosition.Distance( endingPosition ) / CellSize );

		if ( pathCreator == null && startingCell.Occupied ) return false;
		if ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;

		if ( pathCreator == null && endingCell.Occupied ) return false;
		if ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;

		Cell lastCell = startingCell;
		for ( int i = 0; i <= distanceInSteps; i++ )
		{
			var direction = (endingPosition - lastCell.Position).Normal;
			var cellToCheck = GetNeighbourInDirection( lastCell, direction );

			if ( cellToCheck == null ) return false;
			if ( cellToCheck == endingCell ) return true;
			if ( cellToCheck == lastCell ) continue;
			if ( pathCreator == null && cellToCheck.Occupied ) return false;
			if ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;
			if ( !cellToCheck.IsNeighbour( lastCell ) ) return false;

			lastCell = cellToCheck;

			if ( debugShow )
				lastCell.Draw( 2f, false, false, false );
		}

		return true;
	}

	/// <summary>Can you roughly walk towards the cell without it being a direct line of sight</summary>
	public bool IsDirectlyWalkable( Cell startingCell, Cell endingCell, float maxDistanceFromDirectPath = 150f, Component pathCreator = null, bool withConnections = true )
	{
		if ( startingCell == null || endingCell == null ) return false;

		var currentCell = startingCell;
		var directPath = new Line( startingCell.Position.WithZ( 0 ), endingCell.Position.WithZ( 0 ) );
		List<Cell> cellsChecked = new();

		if ( pathCreator == null && startingCell.Occupied ) return false;
		if ( pathCreator != null && startingCell.Occupied && startingCell.OccupyingEntity != pathCreator ) return false;

		if ( pathCreator == null && endingCell.Occupied ) return false;
		if ( pathCreator != null && endingCell.Occupied && endingCell.OccupyingEntity != pathCreator ) return false;

		while ( currentCell != endingCell && directPath.Distance( currentCell.Position.WithZ( 0 ) ) <= maxDistanceFromDirectPath )
		{
			var cellToCheck = withConnections ? currentCell.GetClosestNeighbourAndConnection( endingCell.Position ) : currentCell.GetClosestNeighbour( endingCell.Position );

			if ( cellToCheck == null ) return false;
			if ( cellsChecked.Contains( cellToCheck ) ) return false;
			if ( pathCreator == null && cellToCheck.Occupied ) return false;
			if ( pathCreator != null && cellToCheck.Occupied && cellToCheck.OccupyingEntity != pathCreator ) return false;

			if ( cellToCheck == endingCell ) return true;

			cellsChecked.Add( currentCell );
			currentCell = cellToCheck;
		}

		return false;
	}

	public bool IsInsideBounds( Vector3 point ) => Bounds.IsRotatedPointWithinBounds( Position, point, Rotation );
	public bool IsInsideCylinder( Vector3 point ) => Bounds.IsInsideSquishedRotatedCylinder( Position, point, Rotation );

	public void Initialize()
	{
		if ( Grids.ContainsKey( Identifier ) )
		{
			if ( Grids[Identifier] != null )
				Grids[Identifier].Delete( true );

			Grids[Identifier] = this;
		}
		else
			Grids.Add( Identifier, this );
	}

	public void Delete( bool deleteSave = false )
	{
		if ( Grids.ContainsKey( Identifier ) )
		{
			Grids[Identifier] = null;
			Grids.Remove( Identifier );
		}
	}

	public List<Cell> GetCellsInBBox( BBox bbox )
	{
		var cells = new List<Cell>();

		foreach ( var cell in AllCells )
			if ( bbox.Contains( cell.Position ) )
				cells.Add( cell );

		return cells;
	}

	public override int GetHashCode() => Settings.GetHashCode();

	/// <summary>Gives the edge tag to all cells with less than 8 neighbours</summary>
	public async Task AssignEdgeCells( int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" ) => await assignEdgeCellsInternal( AllCells.ToList(), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );

	public async Task AssignEdgeCells( BBox bounds, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" ) => await assignEdgeCellsInternal( GetCellsInBBox( bounds ), maxNeighourCount, threadsToUse, clearTags, tagToExclude, tagToAssign );

	internal async Task assignEdgeCellsInternal( List<Cell> cells, int maxNeighourCount = 8, int threadsToUse = 1, bool clearTags = false, string tagToExclude = "", string tagToAssign = "edge" )
	{
		var cellsCount = cells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = cells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					if ( clearTags )
						cell.Tags.Remove( tagToAssign );

					var neighbours = cell.GetNeighbours();

					if ( tagToExclude != "" )
						neighbours = neighbours.Where( x => !x.Tags.Has( tagToExclude ) );

					if ( neighbours.Count() < maxNeighourCount )
						cell.Tags.Add( tagToAssign );
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	/// <summary>Adds the droppable connection to cells you can drop from</summary>
	public async Task AssignDroppableCells( int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( "edge" ).ToList(), threadsToUse );

	public async Task AssignDroppableCells( BBox bounds, int threadsToUse = 1 ) => await internalAssignDroppableCells( CellsWithTag( bounds, "edge" ).ToList(), threadsToUse );

	internal async Task internalAssignDroppableCells( List<Cell> cells, int threadsToUse = 1 )
	{
		var allCells = cells;
		var cellsCount = allCells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					var droppableCell = cell.GetFirstValidDroppable( maxHeightDistance: MaxDropHeight );
					if ( droppableCell != null )
						cell.AddConnection( droppableCell, "drop" );
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	public IEnumerable<Cell> JumpableCandidates()
	{
		var droppedCells = CellsWithConnection( "drop" ).SelectMany( cell => cell.GetConnections( "drop" ).Select( connection => connection.Current ) );
		return CellsWithTag( "edge" ).Concat( droppedCells );
	}

	public async Task AssignJumpableCells( JumpDefinition definition, int threadsToUse = 16 ) => await internalAssignJumpableCells( JumpableCandidates().ToList(), definition, threadsToUse );

	internal async Task internalAssignJumpableCells( List<Cell> cells, JumpDefinition definition, int threadsToUse = 16 )
	{
		var allCells = cells;
		var cellsCount = allCells.Count();
		threadsToUse = Math.Max( 1, threadsToUse );
		var cellsEachThread = (int)(cellsCount / threadsToUse);
		var lastThreadCount = cellsCount - (cellsEachThread * (threadsToUse - 1));
		List<Task> tasks = new();

		for ( int i = 0; i < threadsToUse; i++ )
		{
			var curentThread = i;

			tasks.Add( GameTask.RunInThreadAsync( () =>
			{
				var totalFraction = 1f;
				var cellsRange = curentThread == threadsToUse - 1 ? cellsEachThread : lastThreadCount;
				var cellsToCheck = allCells.Skip( cellsEachThread * curentThread ).Take( cellsRange );

				foreach ( var cell in cellsToCheck )
				{
					if ( totalFraction >= 1f )
					{
						List<Cell> connectedCells = new();
						List<AStarNode> jumpConnections = new();

						foreach ( var jumpableCell in cell.GetValidJumpables( definition, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps ) )
							if ( jumpableCell != null )
							{
								jumpConnections.Add( cell.AddConnection( jumpableCell, definition.Name ) );
								connectedCells.Add( jumpableCell );
							}

						foreach ( var jumpableConnection in connectedCells )
						{
							var direction = (cell.Position - jumpableConnection.Position).WithZ( 0 ).Normal;
							var jumpbackCell = jumpableConnection.GetValidJumpable( definition, direction, MaxDropHeight, IgnoreConnectionsForJumps, IgnoreLOSForJumps );

							if ( jumpbackCell != null )
								if ( !IsDirectlyWalkable( jumpbackCell, cell ) )
								{
									var duplicate = false;
									foreach ( var connection in jumpConnections )
										if ( connection.Parent.Current == jumpbackCell && connection.MovementTag == definition.Name )
											duplicate = true;
									if ( !duplicate )
										jumpConnections.Add( jumpableConnection.AddConnection( jumpbackCell, definition.Name ) );
								}
						}

						foreach ( var connection in jumpConnections )
							if ( LineOfSight( connection.Parent.Current, connection.Current ) )
								connection.Parent.Current.RemoveConnection( connection );

						totalFraction = 0f;
					}

					totalFraction += definition.GenerateFraction;
				}
			} ) );
		}

		await GameTask.WhenAll( tasks );
	}

	public Vector3 TraceParabola( Vector3 startingPosition, Vector3 horizontalVelocity, float verticalSpeed, float gravity, float maxDropHeight, int subSteps = 2 )
	{
		var horizontalDirection = horizontalVelocity.WithZ( 0 ).Normal;
		var horizontalSpeed = horizontalVelocity.WithZ( 0 ).Length;
		var maxHeight = startingPosition.z + MathAStar.ParabolaMaxHeight( verticalSpeed, gravity );
		var minHeight = maxHeight - maxDropHeight;
		var currentDistance = 1;
		var lastPositionChecked = startingPosition;

		while ( lastPositionChecked.z >= minHeight )
		{
			var horizontalOffset = CellSize * currentDistance / subSteps;
			var verticalOffset = MathAStar.ParabolaHeight( horizontalOffset, horizontalSpeed, verticalSpeed, gravity );
			var nextPositionToCheck = startingPosition + horizontalDirection * horizontalOffset + Vector3.Up * verticalOffset;

			var clearanceBBox = new BBox( new Vector3( -WidthClearance / 2f, -WidthClearance / 2f, StepSize ), new Vector3( WidthClearance / 2f, WidthClearance / 2f, HeightClearance ) );
			var jumpTrace = Scene.Trace.Box( clearanceBBox, lastPositionChecked, nextPositionToCheck )
				.WithGridSettings( Settings )
				.Run();

			if ( jumpTrace.Hit )
				return jumpTrace.EndPosition;

			lastPositionChecked = nextPositionToCheck;
			currentDistance++;
		}

		return lastPositionChecked;
	}

	public void RemoveCells( BBox bounds, bool printInfo = false )
	{
		var cellsToRemove = GetCellsInBBox( bounds );
		var count = cellsToRemove.Count();

		foreach ( var cell in cellsToRemove )
			cell.Delete();

		if ( printInfo )
			Print( $"Removed {count} cells" );
	}

	public async Task GenerateCells( BBox bounds, int threadedChunkSides = 1, bool printInfo = true )
	{
		List<Task<List<Cell>>> tasks = new();
		var totalMins = bounds.Mins;
		var totalMaxs = bounds.Maxs;
		var totalSize = bounds.Size;

		threadedChunkSides = Math.Max( 1, threadedChunkSides );

		for ( int x = 1; x <= threadedChunkSides; x++ )
		{
			for ( int y = 1; y <= threadedChunkSides; y++ )
			{
				var xOffset = totalSize.x / threadedChunkSides * x - totalSize.x / threadedChunkSides / 2;
				var yOffset = totalSize.y / threadedChunkSides * y - totalSize.y / threadedChunkSides / 2;
				var offset = new Vector3( xOffset, yOffset );
				var chunkSize = totalSize / threadedChunkSides;
				var chunkMins = totalMins + offset - chunkSize / 2;
				var chunkMaxs = totalMins + offset + chunkSize / 2;
				var dividedBounds = new BBox( chunkMins.WithZ( totalMins.z ), chunkMaxs.WithZ( totalMaxs.z ) );

				tasks.Add( GameTask.RunInThreadAsync( () => createCells( dividedBounds, printInfo ) ) );
			}
		}

		await GameTask.WhenAll( tasks );

		foreach ( var task in tasks )
			foreach ( var cell in task.Result )
				AddCell( cell );
	}

	/// <summary>Create cells in that local bbox (Doesn't add them)</summary>
	private List<Cell> createCells( BBox bounds, bool printInfo = true )
	{
		var generatedCells = new List<Cell>();

		var minimumGrid = bounds.Mins.ToIntVector2( CellSize );
		var maximumGrid = bounds.Maxs.ToIntVector2( CellSize );
		var startingColumn = minimumGrid.y - MinimumColumn;
		var totalColumns = maximumGrid.y - minimumGrid.y;
		var endingColumn = startingColumn + totalColumns;
		var startingRow = minimumGrid.x - MinimumRow;
		var totalRows = maximumGrid.x - minimumGrid.x;
		var endingRow = startingRow + totalRows;

		for ( int column = startingColumn; column < endingColumn; column++ )
		{
			for ( int row = startingRow; row < endingRow; row++ )
			{
				var startPosition = WorldBounds.Mins.WithZ( WorldBounds.Maxs.z ) + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, Tolerance * 2f ) * AxisRotation;
				var endPosition = WorldBounds.Mins + new Vector3( row * CellSize + CellSize / 2f, column * CellSize + CellSize / 2f, -Tolerance ) * AxisRotation;
				var checkBBox = new BBox( new Vector3( -CellSize / 2f + Tolerance, -CellSize / 2f + Tolerance, 0f ), new Vector3( CellSize / 2f - Tolerance, CellSize / 2f - Tolerance, 0.001f ) );
				var positionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )
					.WithGridSettings( Settings );

				var positionResult = positionTrace.Run();

				while ( positionResult.Hit && startPosition.z >= endPosition.z )
				{
					DebugCastsHit++;
					if ( IsInsideBounds( positionResult.HitPosition ) )
					{
						if ( !CylinderShaped || IsInsideCylinder( positionResult.HitPosition ) )
						{
							var angle = Vector3.GetAngle( Vector3.Up, positionResult.Normal );
							if ( angle <= StandableAngle )
							{
								var newCell = Cell.TryCreate( this, positionResult.HitPosition );

								if ( newCell != null )
									generatedCells.Add( newCell );
							}
							else
							{
								DebugAngleRejected++;
							}
						}
					}
					else
					{
						DebugOutOfBounds++;
					}

					startPosition = positionResult.HitPosition + Vector3.Down * HeightClearance;

					// Scene-System port of Sandbox.Trace.TestPoint: a zero-length sphere trace reports
					// StartedSolid when the point is inside geometry. Step down until we're clear.
					while ( Scene.Trace.Sphere( CellSize / 2f - Tolerance, startPosition, startPosition ).Run().StartedSolid )
						startPosition += Vector3.Down * HeightClearance;

					positionTrace = Scene.Trace.Box( checkBBox, startPosition, endPosition )
						.WithGridSettings( Settings );

					positionResult = positionTrace.Run();
				}
			}
		}

		return generatedCells;
	}

	public IEnumerable<Cell> CellsWithTag( string tag ) => AllCells.Where( cell => cell.Tags.Has( tag ) );
	public IEnumerable<Cell> CellsWithTags( params string[] tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTags( List<string> tags ) => AllCells.Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTag( BBox bounds, string tag ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tag ) );
	public IEnumerable<Cell> CellsWithTags( BBox bounds, params string[] tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithTags( BBox bounds, List<string> tags ) => GetCellsInBBox( bounds ).Where( cell => cell.Tags.Has( tags ) );
	public IEnumerable<Cell> CellsWithConnection( string movementTag ) => AllCells.Where( cell => cell.GetConnections( movementTag ).Count() > 0 );
	public IEnumerable<Cell> CellsWithConnection( BBox bounds, string movementTag ) => GetCellsInBBox( bounds ).Where( cell => cell.GetConnections( movementTag ).Count() > 0 );

	public void CheckOccupancy( string tag )
	{
		foreach ( var cell in AllCells )
			cell.Occupied = cell.TestForOccupancy( tag );
	}
}