editor/AdvancedMappingTools/Procedural/Seeds.cs

Procedural seeding helpers for the AdvancedMappingTools editor tooling. StableSeed hashes a master seed with a part path so generation order cannot change results, SeedStream is a small xorshift random stream, and SeedSet stores per-category seeds with lock support.

namespace AdvancedMappingTools;

/// <summary>
/// Turns a seed and a stable part path into a new seed. The result depends on nothing else,
/// so the order parts are generated in can never change what a part looks like.
/// </summary>
public static class StableSeed
{
	public static int Of( int seed, string path )
	{
		var hash = (2166136261u ^ (uint)seed) * 16777619u;

		foreach ( var c in path )
		{
			hash ^= c;
			hash *= 16777619u;
		}

		// Spreads the bits, paths that differ in one letter should not give close seeds.
		hash ^= hash >> 15;
		hash *= 0x2c1b3c6du;
		hash ^= hash >> 12;
		hash *= 0x297a2d39u;
		hash ^= hash >> 15;

		return (int)(hash & 0x7fffffff);
	}
}

/// <summary>
/// A small random stream with a fixed algorithm, so results never change with the runtime.
/// </summary>
public struct SeedStream
{
	uint _state;

	public SeedStream( int seed )
	{
		_state = (uint)seed * 2654435761u + 1u;
		if ( _state == 0u ) _state = 1u;
	}

	/// <summary>
	/// The next value from 0 up to but not including 1.
	/// </summary>
	public float Next()
	{
		_state ^= _state << 13;
		_state ^= _state >> 17;
		_state ^= _state << 5;

		return (_state >> 8) * (1.0f / 16777216.0f);
	}

	public float Range( float min, float max ) => min + (max - min) * Next();

	public bool Chance( float probability ) => Next() < probability;
}

/// <summary>
/// One independent seed per category of a generated object. Changing the roof seed
/// cannot touch windows or walls because they never read it.
/// </summary>
public sealed class SeedSet
{
	public static readonly string[] Names = ["wall", "floor", "frame", "glass", "door", "stair", "roof"];

	public int Master { get; set; } = 1;
	public Dictionary<string, int> Categories { get; set; } = [];
	public List<string> Locked { get; set; } = [];

	public int For( string category ) =>
		Categories.TryGetValue( category, out var seed ) ? seed : StableSeed.Of( Master, category );

	public bool IsLocked( string category ) => Locked.Contains( category );

	public void SetLocked( string category, bool locked )
	{
		Locked.Remove( category );
		if ( locked ) Locked.Add( category );
	}

	/// <summary>
	/// Give one category a new seed. A locked category keeps its seed.
	/// </summary>
	public bool Randomize( string category, int? value = null )
	{
		if ( IsLocked( category ) ) return false;

		Categories[category] = value ?? Random.Shared.Next( 1, int.MaxValue );
		return true;
	}

	public void RandomizeAll( IEnumerable<string> names )
	{
		foreach ( var name in names )
			Randomize( name );
	}
}