Editor/Layers/ArchKinds.cs

Registry for architecture kind manifests used by the editor. It loads a fixed set of IArchKindManifest implementations, indexes them by ident and payload type, detects name/type collisions, and exposes lookup helpers (label, glyph, stage, valid children, root kinds, rename, display name). Also provides a static helper that reloads the table on each call.

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

namespace Sunless.Architecture;

// The third instance of the shape ArchDrawers and ArchAnswers already are: core declares the contract, a module
// answers it, core asks by name. Never cached across an ask - hotload carries statics over, and a stale kind table
// is a plan read with the wrong slot names, which is a map that opens missing half of itself.
public sealed class ArchKinds
{
	readonly Dictionary<string, IArchKindManifest> byIdent = new( StringComparer.Ordinal );
	readonly Dictionary<Type, IArchKindManifest> byPayload = new();
	readonly List<string> collisions = new();

	public static ArchKinds Load()
	{
		var built = new ArchKinds();

		built.Enrol( new ArchBuildingManifest() );
		built.Enrol( new ArchRoomManifest() );
		built.Enrol( new ArchStoryManifest() );
		built.Enrol( new ArchWallManifest() );
		built.Enrol( new ArchWallModManifest() );
		built.Enrol( new ArchOpeningManifest() );
		built.Enrol( new ArchTrimManifest() );
		built.Enrol( new ArchBalconyManifest() );
		built.Enrol( new ArchLadderManifest() );
		built.Enrol( new ArchRoofManifest() );
		built.Enrol( new ArchRoofLightManifest() );
		built.Enrol( new ArchDownpipeManifest() );
		built.Enrol( new ArchStairManifest() );
		built.Enrol( new ArchStairStepManifest() );
		built.Enrol( new ArchStairGuardManifest() );
		built.Enrol( new ArchCarvedStairManifest() );
		built.Enrol( new ArchExteriorStairManifest() );
		built.Enrol( new ArchApproachManifest() );
		built.Enrol( new ArchPillarManifest() );
		built.Enrol( new ArchSpanManifest() );
		built.Enrol( new ArchPlatformManifest() );
		built.Enrol( new ArchBeamManifest() );
		built.Enrol( new ArchCutManifest() );
		built.Enrol( new ArchAssemblyManifest() );
		built.Enrol( new ArchAssetInstanceManifest() );
		built.Enrol( new ArchFenceManifest() );
		built.Enrol( new ArchPipeManifest() );
		built.Enrol( new ArchBracketManifest() );
		built.Enrol( new ArchPorchManifest() );
		built.Enrol( new ArchWalkwayManifest() );
		built.Enrol( new ArchRoadManifest() );
		built.Enrol( new ArchCrossingManifest() );
		built.Enrol( new ArchBridgeManifest() );
		built.Enrol( new ArchTunnelManifest() );

		return built;
	}

	// Refuses rather than replaces, because two manifests claiming one ident is an authoring mistake and the loser
	// would be decided by filesystem order. The clash is kept so a case can report it by name.
	public ArchKinds Enrol( IArchKindManifest manifest )
	{
		if ( manifest?.Kind.Known != true )
		{
			return this;
		}

		if ( byIdent.TryGetValue( manifest.Kind.Ident, out var heldIdent ) )
		{
			collisions.Add( $"{manifest.Kind.Ident} is claimed by both {heldIdent.GetType().Name} and {manifest.GetType().Name}" );

			return this;
		}

		// A storey owns no authored type - it is projected from the floors its rooms stand on - so a kind with no
		// payload is filed by ident alone rather than refused.
		if ( manifest.Payload is not null && byPayload.TryGetValue( manifest.Payload, out var heldPayload ) )
		{
			collisions.Add( $"{manifest.Payload.Name} is claimed by both {heldPayload.Kind.Ident} and {manifest.Kind.Ident}" );

			return this;
		}

		byIdent[manifest.Kind.Ident] = manifest;

		if ( manifest.Payload is not null )
		{
			byPayload[manifest.Payload] = manifest;
		}

		return this;
	}

	// An ident nothing claims is a kind this build has no manifest for - a plan may still carry its parts, so it
	// answers nothing rather than erroring.
	public IArchKindManifest For( string ident )
	{
		return ident is not null && byIdent.TryGetValue( ident, out var manifest ) ? manifest : null;
	}

	public IArchKindManifest For( ArchKind kind ) => For( kind.Ident );

	public IArchKindManifest Claiming( Type payload )
	{
		return payload is not null && byPayload.TryGetValue( payload, out var manifest ) ? manifest : null;
	}

	public IArchKindManifest Claiming( object payload ) => payload is null ? null : Claiming( payload.GetType() );

	// A kind named by hand rather than read off a plan, so the case an author typed is forgiven. Exact match first,
	// or two idents differing only in case would resolve by whichever the table happened to hold first.
	public IArchKindManifest Resolve( string ident )
	{
		return For( ident )
			?? byIdent.Values.FirstOrDefault( manifest => string.Equals( manifest.Kind.Ident, ident, StringComparison.OrdinalIgnoreCase ) );
	}

	public IEnumerable<string> Installed => byIdent.Keys.OrderBy( ident => ident );

	// The same table with one kind gone, which is what a plan carrying that kind looks like to a build that cannot
	// read it - and the negative control every kind case stands on.
	public ArchKinds Without( string ident )
	{
		var built = new ArchKinds();

		foreach ( var manifest in byIdent.Where( entry => entry.Key != ident ).Select( entry => entry.Value ) )
		{
			built.Enrol( manifest );
		}

		return built;
	}

	public ArchKinds Without( ArchKind kind ) => Without( kind.Ident );

	public IReadOnlyCollection<IArchKindManifest> All => byIdent.Values;

	public IReadOnlyList<string> Collisions => collisions;

	// The kinds a parent of this kind may hold, assembled from the manifests rather than from a dictionary that
	// hotload would carry over stale - the exact bug the enum's vacant ordinal was left behind to prevent.
	public IReadOnlyList<ArchKind> ValidChildren( ArchKind parentKind )
	{
		return For( parentKind )?.Children.Where( child => For( child ) is not null ).ToList()
			?? (IReadOnlyList<ArchKind>)Array.Empty<ArchKind>();
	}

	// The kinds that stand at the plan root rather than inside a host. Asked rather than hard-named, or the root of
	// the tree is the one place core still lists Building and Road by hand.
	public IReadOnlyList<ArchKind> RootKinds()
	{
		return byIdent.Values.Where( manifest => manifest.RootsAtPlan ).Select( manifest => manifest.Kind ).ToList();
	}

	public bool RootsAtPlan( ArchKind kind ) => For( kind )?.RootsAtPlan == true;

	public ArchLayerStage Stage( ArchKind kind, object payload = null )
	{
		return For( kind )?.Stage( payload ) ?? ArchLayerStage.Shape;
	}

	public ArchLayerDomain Domain( ArchKind kind ) => For( kind )?.Domain ?? ArchLayerDomain.Buildings;

	public string Label( ArchKind kind ) => For( kind )?.Label ?? kind.Ident;

	public string Glyph( ArchKind kind ) => For( kind )?.Glyph ?? "crop_square";

	public string Subtool( ArchKind kind ) => For( kind )?.Subtool ?? "";

	public string HostOutput( ArchKind kind ) => For( kind )?.HostOutput ?? "a host output";

	public string NameOf( ArchKind kind, object payload, int id )
	{
		return For( kind )?.NameOf( payload, id ) ?? $"{Label( kind )} {id}";
	}

	// Keyed on the payload, because a rename is asked of a thing the tree is holding rather than of a kind.
	public bool TryRename( object payload, string name ) => Claiming( payload )?.TryRename( payload, name ) == true;

	// Null when the kind has no title of its own, which is the tree falling back to the node's stored name.
	public string DisplayName( ArchKind kind, object payload ) => For( kind )?.DisplayName( payload );
}

// The same table for a caller holding none of its own - an inspector row, a menu, an mcp tool. Load walks the type
// library on every ask for the reason ArchDrawers and ArchAnswers both refuse to cache: hotload carries statics over,
// and a stale kind table answers with the manifests the editor had before the edit. A caller in a paint loop should
// hold one Load in a local rather than reach through here per row.
public static class ArchKindsAsked
{
	public static ArchLayerStage Stage( ArchKind kind, object payload = null ) => ArchKinds.Load().Stage( kind, payload );

	public static ArchLayerDomain Domain( ArchKind kind ) => ArchKinds.Load().Domain( kind );

	public static string Label( ArchKind kind ) => ArchKinds.Load().Label( kind );

	public static string Glyph( ArchKind kind ) => ArchKinds.Load().Glyph( kind );

	public static string Subtool( ArchKind kind ) => ArchKinds.Load().Subtool( kind );

	public static string HostOutput( ArchKind kind ) => ArchKinds.Load().HostOutput( kind );

	public static string NameOf( ArchKind kind, object payload, int id ) => ArchKinds.Load().NameOf( kind, payload, id );

	public static bool TryRename( object payload, string name ) => ArchKinds.Load().TryRename( payload, name );

	public static IReadOnlyList<ArchKind> ValidChildren( ArchKind parentKind ) => ArchKinds.Load().ValidChildren( parentKind );

	public static IReadOnlyList<ArchKind> RootKinds() => ArchKinds.Load().RootKinds();

	public static bool RootsAtPlan( ArchKind kind ) => ArchKinds.Load().RootsAtPlan( kind );

	public static string DisplayName( ArchKind kind, object payload ) => ArchKinds.Load().DisplayName( kind, payload );
}