Terrain/TerrainGenerator.cs
using Sandbox.Utility;
using System;
using HC3.Persistence;

namespace HC3.Terrain;

#nullable enable

public class TerrainGeneratorOptions
{
	public int MinHeight { get; set; } = 8;
	public int MaxHeight { get; set; } = 96;
	public int Seed { get; set; } = 5633;
	public int CenterHeight { get; set; } = 12;
	public Curve CentralityCurve { get; set; } = Json.Deserialize<Curve>(
		"""
		[
			{
				"x": 0,
				"y": 0.97606385,
				"in": -0.071428776,
				"out": 0.071428776,
				"mode": "Mirrored"
			},
			{
				"x": 0.38803685,
				"y": 0.9286111,
				"in": -0.008528285,
				"out": 0.008528285,
				"mode": "Mirrored"
			},
			{
				"x": 0.7009202,
				"y": 0,
				"in": 7.400002,
				"out": -7.400002,
				"mode": "Mirrored"
			}
		]
		""" );
}

public sealed class TerrainGenerator : Component,
	Component.ExecuteInEditor,
	ISaveDataProperty<TerrainGeneratorOptions>
{
	[RequireComponent]
	public ParkTerrain Terrain { get; private set; } = null!;

	[Property] public TerrainGeneratorOptions Options { get; set; } = new TerrainGeneratorOptions();

	[Button( icon: "casino" )]
	public void RandomizeSeed()
	{
		Options.Seed = Random.Shared.Next();
		Generate();
	}

	protected override void OnEnabled()
	{
		if ( Scene.IsEditor )
		{
			Generate();
		}
	}

	[Button( icon: "terrain" )]
	public void Generate()
	{
		ClearScenery();

		var data = new TileArraySlice( Terrain.Size );
		var random = new Random( Options.Seed );

		var noise = Noise.SimplexField( new Noise.FractalParameters( random.Next() ) );
		var lakeNoise = Noise.SimplexField( new Noise.FractalParameters( random.Next() ) );
		var paintNoise = Noise.SimplexField( new Noise.FractalParameters( random.Next(), 0.04f, 2 ) );

		var heightMapStride = data.Size.x + 1;
		var heightMapSize = heightMapStride * (data.Size.y + 1);

		var heightMap = new int[heightMapSize];
		var gradientMap = new int[heightMapSize];
		var paintMap = new int[heightMapSize];

		var center = data.Size * 0.5f;
		var start = new Vector2( 32f, data.Size.y * 0.5f );
		var centerRadius = data.Size.x * 0.5f;

		var minHeight = Options.MinHeight;
		var maxHeight = MathX.Lerp( Options.MinHeight, Options.MaxHeight, 0.5f + random.Float() * 0.5f );
		var centerHeight = Options.CenterHeight;

		var curve = Options.CentralityCurve;

		var lakeBias = random.Float( 1.75f, 2.25f );

		for ( var y = 0; y <= data.Size.y; ++y )
		{
			for ( var x = 0; x <= data.Size.x; ++x )
			{
				var noiseHeight = noise.Sample( x, y );
				var centerDist = center.Distance( new Vector2( Math.Max( x, center.x ), y ) );
				var startDist = start.Distance( new Vector2( Math.Max( x, start.x ), y ) );

				var centrality = curve.Evaluate( Math.Clamp( centerDist / centerRadius, 0f, 1f ) );
				var lake = lakeNoise.Sample( x, y ) * Math.Clamp( startDist * 4f / centerRadius, 0f, 1f );

				lake = Math.Clamp( lake * lakeBias - 1f, 0f, 1f );

				var height = MathX.Lerp( minHeight + noiseHeight * (maxHeight - minHeight), centerHeight, centrality );

				height = MathX.Lerp( height, 0f, lake );

				heightMap[x + y * heightMapStride] = (int)height;

				var paintNoiseScale = height.Remap( 10f, 32f, 1f, 16f );

				paintMap[x + y * heightMapStride] = (int)height + (int)((paintNoise.Sample( x, y ) - 0.5f) * paintNoiseScale);
			}
		}

		// Calculate gradients

		for ( var y = 0; y <= data.Size.y; ++y )
		{
			for ( var x = 0; x <= data.Size.x; ++x )
			{
				var height = heightMap[x + y * heightMapStride];

				var left = heightMap[Math.Max( x - 1, 0 ) + y * heightMapStride];
				var right = heightMap[Math.Min( x + 1, data.Size.x ) + y * heightMapStride];

				var top = heightMap[x + Math.Max( y - 1, 0 ) * heightMapStride];
				var bottom = heightMap[x + Math.Min( y + 1, data.Size.y ) * heightMapStride];

				var horz = Math.Abs( right - left );
				var vert = Math.Abs( top - bottom );

				gradientMap[x + y * heightMapStride] = horz + vert;
			}
		}

		var tileset = Terrain.Tileset;

		Span<int> cornerHeights = stackalloc int[4];
		Span<int> paintHeights = stackalloc int[4];
		Span<int> gradients = stackalloc int[4];
		Span<int> cornerMaterials = stackalloc int[4];

		for ( var y = 0; y < data.Size.y; ++y )
		{
			for ( var x = 0; x < data.Size.x; ++x )
			{
				cornerHeights[0] = heightMap[x + y * heightMapStride];
				cornerHeights[1] = heightMap[x + 1 + y * heightMapStride];
				cornerHeights[2] = heightMap[x + (y + 1) * heightMapStride];
				cornerHeights[3] = heightMap[x + 1 + (y + 1) * heightMapStride];

				paintHeights[0] = paintMap[x + y * heightMapStride];
				paintHeights[1] = paintMap[x + 1 + y * heightMapStride];
				paintHeights[2] = paintMap[x + (y + 1) * heightMapStride];
				paintHeights[3] = paintMap[x + 1 + (y + 1) * heightMapStride];

				gradients[0] = gradientMap[x + y * heightMapStride];
				gradients[1] = gradientMap[x + 1 + y * heightMapStride];
				gradients[2] = gradientMap[x + (y + 1) * heightMapStride];
				gradients[3] = gradientMap[x + 1 + (y + 1) * heightMapStride];

				const int sand = 1;
				const int snow = 2;
				const int rock = 3;

				var tile = tileset.FromCornerHeights( cornerHeights, default );

				for ( var i = 0; i < 4; ++i )
				{
					if ( gradients[i] > 3 )
					{
						cornerMaterials[i] = rock;
					}
					else
					{
						cornerMaterials[i] = paintHeights[i] switch
						{
							< 12 => sand,
							> 32 => snow,
							_ => 0
						};
					}
				}

				data[new Vector2Int( x, y )] = tile with { Paint = TilePaint.FromMaterialIndices( cornerMaterials ) };
			}
		}

		Terrain.SetTiles( Terrain.Bounds, data );

		PlaceScenery( data, Options.Seed );

		ITerrainEvent.Post( x => x.Generated( Terrain, Options.Seed ) );
	}

	private void ClearScenery()
	{
		// Only the host should do this.
		if ( !Networking.IsHost )
			return;

		foreach ( var scenery in Scene.GetAllComponents<TerrainScenery>() )
		{
			scenery.DestroyGameObject();
		}
	}

	private void PlaceScenery( TileArraySlice data, int seed )
	{
		// Only the host should do this.
		if ( !Networking.IsHost )
			return;

		var random = new Random( seed );

		(GameObject Prefab, SceneryGenerationOptions Options)[] prefabs = TerrainScenery.AllPrefabs.ToArray();

		if ( prefabs is null )
		{
			Log.Warning( "Couldn't find prefabs!" );
			return;
		}

		var gridManager = GridManager.Instance;

		foreach ( var (prefab, options) in prefabs )
		{
			if ( options.ClusterDensity <= 0f ) continue;
			if ( options.AltitudeRange.Max <= options.AltitudeRange.Min ) continue;

			var noise = Noise.SimplexField( new Noise.FractalParameters( random.Next(), Frequency: 0.04f * (1f - options.ClusterScale), Octaves: 2 ) );
			var heightRangeDenom = 1f / (options.AltitudeRange.Max - options.AltitudeRange.Min);

			foreach ( var (index, tile) in data )
			{
				var noiseFactor = noise.Sample( index.x, index.y );

				if ( tile.MinHeight < options.AltitudeRange.Min || tile.MaxHeight > options.AltitudeRange.Max ) continue;
				if ( tile.Slope.MaxHeightOffset > options.MaxGradient ) continue;

				var height = (tile.MinHeight + tile.MaxHeight) * 0.5f;
				var relHeight = (height - options.AltitudeRange.Min) * heightRangeDenom;

				if ( random.NextSingle() > options.AltitudeCurve.Evaluate( relHeight ) ) continue;

				if ( random.NextSingle() < 1.2f - noiseFactor * (0.3f + options.ClusterDensity * 0.3f) ) continue;
				// if ( gridManager?.IsOccupied( index ) ?? false ) continue;

				var gridPos = new Vector3( Terrain.Bounds.Position + index + 0.5f, tile.MinHeight );
				var worldPos = Terrain.GridToWorld( gridPos );

				var clone = prefab.Clone( transform: new Transform( worldPos ) );

				if ( !Scene.IsEditor )
				{
					clone.NetworkSpawn();
				}
				else
				{
					clone.Flags |= GameObjectFlags.NotSaved;
				}
			}
		}
	}

	string ISaveDataProperty.PropertyName => "TerrainOptions";
	int ISaveDataProperty.PropertyOrder => -2_000;

	TerrainGeneratorOptions ISaveDataProperty<TerrainGeneratorOptions>.WriteValue( Scene scene ) => Options;

	void ISaveDataProperty<TerrainGeneratorOptions>.ReadValue( Scene scene, TerrainGeneratorOptions model )
	{
		Options = model;
	}
}