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.
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 );
}