Editor/Layers/ArchKindManifest.cs

Interfaces and base classes describing metadata and behavior for architecture layer kinds used by the editor. Defines IArchKindManifest with methods for payload typing, filing, naming, geometry and ranks, a concrete base ArchKindManifest with sensible defaults, a generic typed variant ArchKindManifest<TPayload> that implements naming and list filing, and small helper interfaces IArchNamed, IArchReachesWide and IArchNormalizes.

ReflectionFile Access
using System;
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

// Everything core needs to know about one kind of layer, answered by the module that owns it rather than by a
// switch core has to edit. It replaces two parallel taxonomies at once - the arms keyed on the kind, and the arms
// keyed on the payload TYPE - because leaving either behind keeps every module named in core.
public interface IArchKindManifest
{
	ArchKind Kind { get; }

	// The authored type this kind is. One manifest claims one type, or a payload resolves two ways.
	Type Payload { get; }

	// The json property this kind's parts sit under on their host. Empty means the kind is not filed in a list.
	string Slot { get; }

	// The host types parts of this kind are filed on - a flight stands in a room, on a porch deck and on a platform.
	IEnumerable<Type> Hosts { get; }

	ArchLayerDomain Domain { get; }

	string Label { get; }

	string Glyph { get; }

	// The shelf tool that authors this kind. Empty means it is never offered as something to draw.
	string Subtool { get; }

	// Whether this kind stands on the map itself rather than inside a host, which is the tree's root row and the
	// only drop a parentless layer may take.
	bool RootsAtPlan { get; }

	// What a child consumes from a parent of this kind.
	string HostOutput { get; }

	IEnumerable<ArchKind> Children { get; }

	int DirectRank { get; }

	int IndirectRank { get; }

	// Asked of the PAYLOAD, never of the kind alone: a proud wall modifier builds and a carved one voids, and only
	// the part itself knows which.
	ArchLayerStage Stage( object payload );

	string NameOf( object payload, int id );

	// A title the part reads off its own geometry rather than off its stored name - a wall knows which side of the
	// building it faces. Null means the tree shows the name it already holds.
	string DisplayName( object payload );

	bool TryRename( object payload, string name );

	// The parts of this kind filed on that host, in authoring order. The tree supplies the context around them.
	IEnumerable<object> Filed( ArchPlan plan, object host );

	// The list those parts are held IN, so a caller may seat one at an index instead of only at the end. Null when
	// the kind keeps no list of its own.
	System.Collections.IList Folder( ArchPlan plan, object host );

	bool File( object host, object payload );

	bool Unfile( ArchPlan plan, object payload );

	// The outline a nested layer may take as its own, so a trim dropped inside a circular cut runs round the circle.
	IReadOnlyList<List<Vector2>> Loops( object payload );

	float Height( object payload, float fallback );
}

// Answers nothing by default, so a manifest states only what its kind has an opinion about. A kind with no shape,
// no children and no name of its own is a handful of lines.
public abstract class ArchKindManifest : IArchKindManifest
{
	public abstract ArchKind Kind { get; }

	public abstract Type Payload { get; }

	public virtual string Slot => "";

	public virtual IEnumerable<Type> Hosts => Array.Empty<Type>();

	public virtual ArchLayerDomain Domain => ArchLayerDomain.Buildings;

	public virtual string Label => Kind.Ident;

	public virtual string Glyph => "crop_square";

	public virtual string Subtool => "";

	public virtual bool RootsAtPlan => false;

	public virtual string HostOutput => "a host output";

	public virtual IEnumerable<ArchKind> Children => Array.Empty<ArchKind>();

	public virtual int DirectRank => 200;

	public virtual int IndirectRank => 999;

	public virtual ArchLayerStage Stage( object payload ) => ArchLayerStage.Shape;

	public virtual string NameOf( object payload, int id ) => $"{Label} {id}";

	public virtual string DisplayName( object payload ) => null;

	public virtual bool TryRename( object payload, string name ) => false;

	public virtual IEnumerable<object> Filed( ArchPlan plan, object host ) => Array.Empty<object>();

	// Read off the slot the kind already declares for json, so ordered filing costs a module nothing to answer. A
	// kind that roots at the plan is asked with no host, and the plan is what holds its list.
	public virtual System.Collections.IList Folder( ArchPlan plan, object host )
	{
		var holder = host ?? plan;

		return string.IsNullOrEmpty( Slot ) || holder is null
			? null
			: holder.GetType().GetProperty( Slot )?.GetValue( holder ) as System.Collections.IList;
	}

	public virtual bool File( object host, object payload ) => false;

	public virtual bool Unfile( ArchPlan plan, object payload ) => false;

	public virtual IReadOnlyList<List<Vector2>> Loops( object payload ) => Array.Empty<List<Vector2>>();

	public virtual float Height( object payload, float fallback ) => fallback;
}

public interface IArchNamed
{
	string Name { get; set; }
}

// The typed half of the default manifest: payload identity, naming and filing answered off the facts the
// manifest already declares, so a kind with nothing unusual about it states no methods at all.
public abstract class ArchKindManifest<TPayload> : ArchKindManifest where TPayload : class
{
	public override Type Payload => typeof( TPayload );

	public override string NameOf( object payload, int id )
	{
		return payload is TPayload and IArchNamed named ? named.Name : base.NameOf( payload, id );
	}

	public override bool TryRename( object payload, string name )
	{
		if ( string.IsNullOrWhiteSpace( name ) || payload is not TPayload || payload is not IArchNamed named )
		{
			return false;
		}

		named.Name = name;

		return true;
	}

	public override IEnumerable<object> Filed( ArchPlan plan, object host )
	{
		return Hosting( host ) && Folder( plan, host ) is { } folder ? folder.Cast<object>() : Array.Empty<object>();
	}

	public override bool File( object host, object payload )
	{
		if ( payload is not TPayload || !Hosting( host ) || Folder( null, host ) is not { } folder )
		{
			return false;
		}

		folder.Add( payload );

		return true;
	}

	public override bool Unfile( ArchPlan plan, object payload )
	{
		if ( plan is null || payload is not TPayload )
		{
			return false;
		}

		foreach ( var host in ArchPlanHosts.Of( plan, Hosts ) )
		{
			if ( Folder( plan, host ) is { } folder && folder.Contains( payload ) )
			{
				folder.Remove( payload );

				return true;
			}
		}

		return false;
	}

	bool Hosting( object host ) => host is not null && Hosts.Any( type => type.IsInstanceOfType( host ) );
}

// A layer whose effect reaches outside whatever scope holds it, so the build gate may never skip on scope alone.
public interface IArchReachesWide
{
	IEnumerable<object> Wide( ArchPlan plan );
}

// A load-time fixup for plans written before this module changed shape, run in publish order.
public interface IArchNormalizes
{
	void Normalize( ArchPlan plan );
}