editor/AdvancedMappingTools/Procedural/GeneratorRegistry.cs

The procedural generator registry for the mapping tools. Defines the abstract GeneratorSettings base (seeds, variation, JSON round-trip copy), the IPartGenerator interface that turns settings into a PartPlan, and a static GeneratorRegistry that discovers generator types through EditorTypeLibrary and saves, loads and merges their settings as System.Text.Json.

using System.Text.Json;
using System.Text.Json.Serialization;

namespace AdvancedMappingTools;
/// <summary>
/// Settings of a generator. They are stored on the root part in the scene.
/// </summary>
public abstract class GeneratorSettings
{
	public SeedSet Seeds { get; set; } = new();

	[Property, Group( "Variation" ), Range( 0, 1 ), WideMode, Description( "How much parts differ from each other." )]
	public float Variation { get; set; } = 0.3f;

	/// <summary>
	/// Common options these settings have no use for. They stay out of the inspector.
	/// </summary>
	[JsonIgnore] public virtual IReadOnlySet<string> UnusedOptions => null;

	/// <summary>
	/// Take over all values from other settings of the same kind. For presets, which only
	/// have values to give, while a tool keeps showing the object it has.
	/// </summary>
	public void CopyFrom( GeneratorSettings other )
	{
		if ( other is null || other.GetType() != GetType() )
			throw new ArgumentException( "Settings must have the same generator type.", nameof( other ) );
		var copy = (GeneratorSettings)JsonSerializer.Deserialize( JsonSerializer.Serialize( other, other.GetType() ), other.GetType() );
		Seeds = copy.Seeds ?? new SeedSet();
		foreach ( var property in EditorTypeLibrary.GetType( GetType() ).Properties.Where( x => x.HasAttribute<PropertyAttribute>() && x.CanWrite ) )
			property.SetValue( this, property.GetValue( copy ) );
	}
}

/// <summary>
/// Makes a plan from settings. A generator touches no scene and no interface, which
/// is what lets one be tested alone and called by another.
/// </summary>
public interface IPartGenerator
{
	string Id { get; }
	string Title { get; }
	Type SettingsType { get; }

	/// <summary>
	/// The seed categories this generator reads.
	/// </summary>
	IReadOnlyList<string> SeedNames => SeedSet.Names;

	PartPlan Generate( GeneratorSettings settings, GenContext context );
}

/// <summary>
/// Finds generators by id. A new generator only has to exist to be found.
/// </summary>
public static class GeneratorRegistry
{
	public static IEnumerable<IPartGenerator> All =>
		EditorTypeLibrary.GetTypes<IPartGenerator>()
			.Where( x => !x.IsAbstract && !x.IsInterface )
			.Select( x => x.Create<IPartGenerator>() );

	public static IPartGenerator Find( string id ) => All.FirstOrDefault( x => x.Id == id );

	public static string Save( GeneratorSettings settings ) =>
		JsonSerializer.Serialize( settings, settings.GetType() );

	/// <summary>
	/// Options and seeds of other settings on top of what an object already is. Its line, its
	/// size and its connections are no options, they stay.
	/// </summary>
	public static GeneratorSettings Merge( IPartGenerator generator, GeneratorSettings current, GeneratorSettings values )
	{
		var merged = Load( generator, Save( current ) );
		merged.CopyFrom( values );
		return merged;
	}

	public static GeneratorSettings Load( IPartGenerator generator, string json ) =>
		(GeneratorSettings)JsonSerializer.Deserialize( json, generator.SettingsType );
}