Editor/Layers/ArchKinds.cs
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 ArchCabinetManifest() );
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() );
foreach ( var contributed in ArchAddons.Load().Contributed<IArchKindManifest>() ) {
built.Enrol( contributed );
}
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 ) {
if ( For( parentKind ) is not { } parent ) {
return Array.Empty<ArchKind>();
}
// Both directions, because a kind the game contributed cannot be named in a host's Children list - the
// host's manifest ships in a library that has never heard of it - so it claims the parent itself.
return parent.Children.Where( child => For( child ) is not null )
.Concat( byIdent.Values.Where( manifest => manifest.Parents.Contains( parentKind ) ).Select( manifest => manifest.Kind ) )
.Distinct()
.ToList();
}
// The layer a part hangs under, which is not always the list it is stored in.
public int Parent( ArchKind kind, object payload ) => For( kind )?.Parent( payload ) ?? 0;
// 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;
// Defaults TRUE for a kind nothing is known about: a part that cannot be moved is the exception, and hiding the
// widgets on everything unclaimed would be a worse failure than showing them on one that ignores them.
public bool Transforms( ArchKind kind ) => For( kind )?.Transforms != false;
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 );
}