Grid/GridBuilder.cs

Builder struct for creating a Grid. It stores generation settings (sizes, clearances, tags, jump definitions, bounds and options), exposes fluent With... methods to configure them, and an async Create(scene, ...) method that constructs and runs grid generation steps on a Grid instance.

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

namespace GridAStar;

public struct JumpDefinition
{
	public string Name { get; private set; } = "shortjump";
	public float HorizontalSpeed { get; private set; } = 200f;
	public float VerticalSpeed { get; private set; } = 300f;
	public float Gravity { get; private set; } = -800f;
	public int SidesToCheck { get; private set; } = 8;
	public float AngleOffset { get; private set; } = 0f;
	public float GenerateFraction { get; private set; } = 1f;
	public int MaxPerCell { get; private set; } = 2;

	public JumpDefinition( string name, float horizontalSpeed, float verticalSpeed, float gravity, int sidesToCheck = 8, float angleOffset = 0f, float generateFraction = 1f, int maxPerCell = 2 )
	{
		Name = name;
		HorizontalSpeed = horizontalSpeed;
		VerticalSpeed = verticalSpeed;
		Gravity = gravity > 0 ? -gravity : gravity;
		SidesToCheck = sidesToCheck;
		AngleOffset = angleOffset;
		GenerateFraction = generateFraction;
		MaxPerCell = maxPerCell;
	}

	public JumpDefinition( string name, float horizontalSpeed, float verticalSpeed, int sidesToCheck = 8, float angleOffset = 0f, float generateFraction = 1f, int maxPerCell = 2 )
	{
		Name = name;
		HorizontalSpeed = horizontalSpeed;
		VerticalSpeed = verticalSpeed;
		Gravity = -800f; // Scene physics gravity isn't globally accessible; jumps are unused in this game.
		SidesToCheck = sidesToCheck;
		AngleOffset = angleOffset;
		GenerateFraction = generateFraction;
		MaxPerCell = maxPerCell;
	}
}

public struct GridBuilder
{
	public string Identifier { get; private set; } = "main";
	public float StandableAngle { get; private set; } = GridSettings.DEFAULT_STANDABLE_ANGLE;
	public float StepSize { get; private set; } = GridSettings.DEFAULT_STEP_SIZE;
	public float CellSize { get; private set; } = GridSettings.DEFAULT_CELL_SIZE;
	public float HeightClearance { get; private set; } = GridSettings.DEFAULT_HEIGHT_CLEARANCE;
	public float WidthClearance { get; private set; } = GridSettings.DEFAULT_WIDTH_CLEARANCE;
	public bool GridPerfect { get; private set; } = GridSettings.DEFAULT_GRID_PERFECT;
	public bool StaticOnly { get; private set; } = GridSettings.DEFAULT_STATIC_ONLY;
	public float MaxDropHeight { get; private set; } = GridSettings.DEFAULT_DROP_HEIGHT;
	public bool AxisAligned { get; private set; } = false;
	public bool CylinderShaped { get; private set; } = false;
	public List<string> TagsToInclude { get; private set; } = new() { "solid" };
	public List<string> TagsToExclude { get; private set; } = new() { "player" };
	public List<JumpDefinition> JumpDefinitions { get; private set; } = new();
	public int MinNeighbourCount { get; private set; } = 8;
	public bool IgnoreConnectionsForJumps { get; private set; } = false;
	public bool IgnoreLOSForJumps { get; private set; } = false;
	public Vector3 Position { get; private set; } = new();
	public BBox Bounds { get; private set; } = new();
	public Rotation Rotation { get; set; } = new();

	public GridBuilder()
	{
		// Scene-System port: the legacy default read Game.PhysicsWorld.Body.GetBounds(); in the Scene System
		// physics is per-Scene, so callers always set bounds explicitly via WithBounds().
		Position = Vector3.Zero;
		Bounds = new BBox( 0 );
	}

	/// <summary>By default the identifier is "main", which makes it useable with Grid.Main</summary>
	public GridBuilder( string identifier ) : this()
	{
		Identifier = identifier;
	}

	public GridBuilder WithStandableAngle( float standableAngle )
	{
		StandableAngle = Math.Min( standableAngle, 89 );
		return this;
	}

	public GridBuilder WithStepSize( float stepSize )
	{
		if ( !GridPerfect )
			StepSize = stepSize;
		return this;
	}

	public GridBuilder WithCellSize( float cellSize )
	{
		CellSize = cellSize;
		return this;
	}

	public GridBuilder WithHeightClearance( float heightClearance )
	{
		HeightClearance = Math.Max( CellSize / 2, heightClearance );
		return this;
	}

	public GridBuilder WithWidthClearance( float widthClearance )
	{
		WidthClearance = Math.Max( CellSize / 2, widthClearance );
		return this;
	}

	public GridBuilder WithEdgeNeighbourCount( int minNeighbourCount = 8 )
	{
		MinNeighbourCount = minNeighbourCount;
		return this;
	}

	public GridBuilder WithGridPerfect( bool gridPerfect )
	{
		GridPerfect = gridPerfect;
		return this;
	}

	public GridBuilder WithStaticOnly( bool staticOnly )
	{
		StaticOnly = staticOnly;
		return this;
	}

	/// <summary>Only hit entities with the following tags ("solid" is included by default).</summary>
	public GridBuilder WithTags( params string[] tags )
	{
		foreach ( var tag in tags )
		{
			if ( !TagsToInclude.Contains( tag ) )
				TagsToInclude.Add( tag );
			if ( TagsToExclude.Contains( tag ) )
				TagsToExclude.Remove( tag );
		}
		return this;
	}

	/// <summary>Only hit entities without the following tags ("player" is included by default).</summary>
	public GridBuilder WithoutTags( params string[] tags )
	{
		foreach ( var tag in tags )
		{
			if ( !TagsToExclude.Contains( tag ) )
				TagsToExclude.Add( tag );
			if ( TagsToInclude.Contains( tag ) )
				TagsToInclude.Remove( tag );
		}
		return this;
	}

	/// <summary>Clear all include tags so the grid generates on any solid geometry regardless of tags.</summary>
	public GridBuilder WithoutIncludeTags()
	{
		TagsToInclude.Clear();
		return this;
	}

	public GridBuilder WithBounds( Vector3 position, BBox bounds )
	{
		Position = position;
		Bounds = bounds;
		return this;
	}

	public GridBuilder WithBounds( Vector3 position, BBox bounds, Rotation rotation )
	{
		Position = position;
		Bounds = bounds;
		Rotation = rotation;
		return this;
	}

	public GridBuilder WithRotation( Rotation rotation )
	{
		Rotation = rotation;
		return this;
	}

	public GridBuilder WithAxisAligned( bool axisAligned )
	{
		AxisAligned = axisAligned;
		return this;
	}

	public GridBuilder WithCylinderShaped( bool cylinderShaped )
	{
		CylinderShaped = cylinderShaped;
		return this;
	}

	public GridBuilder WithMaxDropHeight( float maxDropHeight )
	{
		MaxDropHeight = maxDropHeight;
		return this;
	}

	public GridBuilder AddJumpDefinition( JumpDefinition jumpDefinition )
	{
		JumpDefinitions.Add( jumpDefinition );
		return this;
	}

	public GridBuilder JumpsIgnoreConnections( bool ignore )
	{
		IgnoreConnectionsForJumps = ignore;
		return this;
	}

	public GridBuilder JumpsIgnoreLOS( bool ignore )
	{
		IgnoreLOSForJumps = ignore;
		return this;
	}

	/// <summary>
	/// Creates a new grid with the settings given, tracing against <paramref name="scene"/>'s physics world.
	/// </summary>
	/// <param name="scene">The scene to trace against.</param>
	/// <param name="threadedChunkSides">How many sides to split generation into (1 = single thread). We pass 1 to stay safe.</param>
	/// <param name="printInfo">Print information about the grid's generation state</param>
	public async Task<Grid> Create( Scene scene, int threadedChunkSides = 1, bool printInfo = true )
	{
		Stopwatch totalWatch = new Stopwatch();
		totalWatch.Start();

		var currentGrid = new Grid( this ) { Scene = scene };

		await currentGrid.GenerateCells( currentGrid.RotatedBounds, threadedChunkSides, printInfo );

		if ( printInfo )
			currentGrid.Print( "Creating grid" );

		await currentGrid.AssignEdgeCells( MinNeighbourCount, threadsToUse: threadedChunkSides * threadedChunkSides );

		if ( MaxDropHeight > 0 )
			await currentGrid.AssignDroppableCells( threadsToUse: threadedChunkSides * threadedChunkSides );

		if ( JumpDefinitions.Count() > 0 )
			foreach ( var definition in JumpDefinitions )
				await currentGrid.AssignJumpableCells( definition, threadsToUse: threadedChunkSides * threadedChunkSides );

		totalWatch.Stop();
		if ( printInfo )
			currentGrid.Print( $"Finished in {totalWatch.ElapsedMilliseconds}ms with {currentGrid.AllCells.Count()} cells" );

		currentGrid.Initialize();

		return currentGrid;
	}

	public override int GetHashCode()
	{
		var identifierHashCode = Identifier.GetHashCode();
		var positionHashCode = Position.GetHashCode();
		var boundsHashCode = Bounds.GetHashCode();
		var rotationHashCode = Rotation.GetHashCode();
		var axisAlignedHashCode = AxisAligned.GetHashCode();
		var standableAngleHashCode = StandableAngle.GetHashCode();
		var stepSizeHashCode = StepSize.GetHashCode();
		var cellSizeHashCode = CellSize.GetHashCode();
		var heightClearanceHashCode = HeightClearance.GetHashCode();
		var widthClearanceHashCode = WidthClearance.GetHashCode();
		var gridPerfectHashCode = GridPerfect.GetHashCode();
		var staticOnlyHashCode = StaticOnly.GetHashCode();
		var maxDropHeightHashCode = MaxDropHeight.GetHashCode();
		var cylinderShapedHashCode = CylinderShaped.GetHashCode();
		var tagsToIncludeHashCode = string.Join( string.Empty, TagsToInclude ).GetHashCode();
		var tagsToExcludeHashCode = string.Join( string.Empty, TagsToExclude ).GetHashCode();

		var hashCodeFirst = HashCode.Combine( identifierHashCode, positionHashCode, boundsHashCode, rotationHashCode, axisAlignedHashCode, standableAngleHashCode, stepSizeHashCode, cellSizeHashCode );
		var hashCodeSecond = HashCode.Combine( cellSizeHashCode, heightClearanceHashCode, widthClearanceHashCode, gridPerfectHashCode, staticOnlyHashCode, maxDropHeightHashCode, cylinderShapedHashCode, tagsToIncludeHashCode );
		var hashCodeThird = HashCode.Combine( tagsToExcludeHashCode );

		return HashCode.Combine( hashCodeFirst, hashCodeSecond, hashCodeThird );
	}
}