Editor-side architecture helper and provider registry. Defines data records, interfaces for architectural services (grading, storeys, walkways, pillars, roofs, roads, etc.), and a sealed ArchAnswers class that holds implementations and dispatches queries. Also provides static ArchAsks convenience methods that load a default set of implementations.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// One storey of a building, named and levelled by the module that owns floors. Subject is that module's own
// payload, handed straight back to its drawer, so the builder files a storey without ever knowing its shape.
public readonly record struct ArchStorey( string Name, int Level, IReadOnlyList<ArchRoom> Rooms, object Subject );
public readonly record struct ArchRoadJunction( string Name, object Subject, ArchExtent Extent );
public interface IArchRoadNetwork
{
IReadOnlyList<ArchRoadJunction> Junctions( ArchPlan plan, ArchKit kit );
}
// How far off the ground a building's whole body stands.
public interface IArchGrade
{
float Lift( ArchPlan plan, ArchBuilding building, ArchKit kit );
}
// The storeys a building draws, in the order they are filed.
public interface IArchStoreys
{
IEnumerable<ArchStorey> Of( ArchBuilding building, ArchKit kit, ArchStyle style );
}
// Whether a room is a link across a gap, and whether that link climbs.
public interface IArchWalkways
{
bool IsWalkway( ArchRoom room );
bool Rising( ArchRoom room );
bool RunsAlongX( ArchRoom room );
}
// The top of whatever a module stands over a patch of ground, so a drag can finish clear of it. Roofs and
// tunnels both answer, so this contract gathers every implementation instead of one.
public interface IArchHeadroom
{
bool Over( ArchPlan plan, ArchKit kit, int level, IReadOnlyList<Vector2> footprint, out float top );
}
public interface IArchPillarGeometry
{
IEnumerable<Vector3> Positions( ArchPillarPart pillar );
float Height( ArchPillarPart pillar, ArchRoom room, ArchKit kit, ArchPlan plan );
int RunCount( ArchPillarPart pillar );
int CarriedCount( ArchPillarPart pillar );
void Stand( ArchMesh canvas, ArchPillarPart pillar, ArchBrush shaft, ArchBrush end );
}
public interface IArchPipeForms
{
void Run( ToolSidebarWidget panel, ArchPipePart part, Action refresh, Action changed );
}
public interface IArchApproachLanding
{
bool TryLand( ArchPlan plan, ArchApproachPart approach, ArchKit kit, float drag, out float run, out float height );
float Back( ArchRoadPart road, ArchKit kit );
}
public interface IArchDownpipePlacement
{
ArchDownpipePart Place( ArchPlan plan, ArchBuilding building, ArchKit kit, Vector2 point, string profile, bool shoe, PipeRun run );
}
public interface IArchPillarForms
{
void Kinds( ToolSidebarWidget panel, IReadOnlyList<ArchPillarType> offered, string chosen, Action<ArchPillarType> adopt );
void Build( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed );
void Sizes( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed );
ArchPillarPart Retype( ArchTool tool, ArchPillarPart pillar, ArchPillarType type );
}
// A DECK SURFACE, finer than the plane the section names. The module that owns roofs lofts real patches over that
// plane, so a light cut into one and plant standing on one take the patch height rather than an authored z; a deck
// with nobody to loft it is met on its nominal plane instead.
public interface IArchRoofDecks
{
float Seat( ArchRoofPart roof, ArchKit kit, Vector2 at );
bool Struck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on );
}
// The query half of what ArchDrawers is for generation: one implementation per contract, reached through the role
// rather than by name, so a caller measures a roof deck without naming the generator that builds one. Never cached
// across a build - hotload carries statics over, and a stale table would answer with the code the editor had before
// the edit. A contract left out of Load answers nothing, which every ask falls back from rather than erroring.
public sealed class ArchAnswers
{
readonly Dictionary<Type, object> answers = new();
readonly Dictionary<Type, List<object>> everyone = new();
public static ArchAnswers Load()
{
var built = new ArchAnswers();
built.Answers<IArchGrade>( new ArchFloorGrade() );
built.Answers<IArchStoreys>( new ArchFloorStoreys() );
built.Answers<IArchWalkways>( new ArchWalkwayLinks() );
built.Answers<IArchPillarGeometry>( new ArchPillarGeometry() );
built.Answers<IArchPipeForms>( new ArchPipeForms() );
built.Answers<IArchApproachLanding>( new ArchApproachLanding() );
built.Answers<IArchWallRakes>( new ArchStairWallRakes() );
built.Answers<IArchDownpipePlacement>( new ArchDownpipePlacement() );
built.Answers<IArchPillarForms>( new ArchPillarForms() );
built.Answers<IArchRoadNetwork>( new ArchRoadNetwork() );
built.Answers<IArchRoofDecks>( new ArchRoofDecks() );
built.AllAnswer<IArchHeadroom>( new ArchRoofHeadroom(), new ArchTunnelHeadroom() );
return built;
}
public T Asks<T>() where T : class => answers.TryGetValue( typeof( T ), out var answer ) ? answer as T : null;
public IEnumerable<T> AsksAll<T>() where T : class
{
return everyone.TryGetValue( typeof( T ), out var found ) ? found.OfType<T>() : Array.Empty<T>();
}
// The same table with one contract unanswered, which is the negative control every fallback is proved against.
public ArchAnswers Without<T>() where T : class
{
var built = new ArchAnswers();
foreach ( var answer in answers.Where( entry => entry.Key != typeof( T ) ) )
{
built.answers[answer.Key] = answer.Value;
}
foreach ( var answer in everyone.Where( entry => entry.Key != typeof( T ) ) )
{
built.everyone[answer.Key] = answer.Value;
}
return built;
}
public float Lift( ArchPlan plan, ArchBuilding building, ArchKit kit )
{
return Asks<IArchGrade>() is IArchGrade grade ? grade.Lift( plan, building, kit ) : 0f;
}
public IEnumerable<ArchStorey> Storeys( ArchBuilding building, ArchKit kit, ArchStyle style )
{
return Asks<IArchStoreys>() is IArchStoreys storeys
? storeys.Of( building, kit, style )
: Array.Empty<ArchStorey>();
}
public bool IsWalkway( ArchRoom room ) => Asks<IArchWalkways>() is IArchWalkways walkways && walkways.IsWalkway( room );
public bool Rising( ArchRoom room ) => Asks<IArchWalkways>() is IArchWalkways walkways && walkways.Rising( room );
public bool WalkwayRunsAlongX( ArchRoom room )
{
return Asks<IArchWalkways>() is IArchWalkways walkways && walkways.RunsAlongX( room );
}
public IReadOnlyList<ArchWallRake> WallRakes(
ArchWall wall,
ArchRoom room,
ArchBuilding building,
ArchPlan plan,
ArchKit kit,
float half,
float wallHeight )
{
return Asks<IArchWallRakes>() is { } rakes
? rakes.Along( wall, room, building, plan, kit, half, wallHeight )
: Array.Empty<ArchWallRake>();
}
public IReadOnlyList<ArchRoadJunction> RoadJunctions( ArchPlan plan, ArchKit kit )
{
return Asks<IArchRoadNetwork>() is IArchRoadNetwork roads
? roads.Junctions( plan, kit )
: Array.Empty<ArchRoadJunction>();
}
public IEnumerable<Vector3> PillarPositions( ArchPillarPart pillar )
{
return Asks<IArchPillarGeometry>() is IArchPillarGeometry geometry
? geometry.Positions( pillar )
: Array.Empty<Vector3>();
}
public float PillarHeight( ArchPillarPart pillar, ArchRoom room, ArchKit kit, ArchPlan plan = null )
{
return Asks<IArchPillarGeometry>() is IArchPillarGeometry geometry
? geometry.Height( pillar, room, kit, plan )
: pillar?.Height ?? 0f;
}
public int PillarRunCount( ArchPillarPart pillar )
{
return Asks<IArchPillarGeometry>() is IArchPillarGeometry geometry ? geometry.RunCount( pillar ) : 0;
}
public int PillarCarriedCount( ArchPillarPart pillar )
{
return Asks<IArchPillarGeometry>() is IArchPillarGeometry geometry ? geometry.CarriedCount( pillar ) : 0;
}
public void StandPillar( ArchMesh canvas, ArchPillarPart pillar, ArchBrush shaft, ArchBrush end )
{
if ( Asks<IArchPillarGeometry>() is IArchPillarGeometry geometry )
{
geometry.Stand( canvas, pillar, shaft, end );
}
}
public void PillarKinds( ToolSidebarWidget panel, IReadOnlyList<ArchPillarType> offered, string chosen, Action<ArchPillarType> adopt )
{
if ( Asks<IArchPillarForms>() is IArchPillarForms forms )
{
forms.Kinds( panel, offered, chosen, adopt );
}
}
public void BuildPillar( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
{
if ( Asks<IArchPillarForms>() is IArchPillarForms forms )
{
forms.Build( panel, pillar, type, refresh, changed );
}
}
public void SizePillar( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
{
if ( Asks<IArchPillarForms>() is IArchPillarForms forms )
{
forms.Sizes( panel, pillar, type, refresh, changed );
}
}
public ArchPillarPart RetypePillar( ArchTool tool, ArchPillarPart pillar, ArchPillarType type )
{
return Asks<IArchPillarForms>() is IArchPillarForms forms ? forms.Retype( tool, pillar, type ) : pillar;
}
public ArchDownpipePart PlaceDownpipe(
ArchPlan plan,
ArchBuilding building,
ArchKit kit,
Vector2 point,
string profile,
bool shoe,
PipeRun run )
{
return Asks<IArchDownpipePlacement>() is IArchDownpipePlacement placement
? placement.Place( plan, building, kit, point, profile, shoe, run )
: null;
}
public float RoofSeat( ArchRoofPart roof, ArchKit kit, Vector2 at )
{
return Asks<IArchRoofDecks>() is { } decks
? decks.Seat( roof, kit, at )
: ArchRoofPlane.At( roof, at ) + ArchRoofPlane.Thickness( roof, kit );
}
public bool RoofStruck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on )
{
hit = default;
on = null;
return Asks<IArchRoofDecks>() is { } decks && decks.Struck( plan, level, kit, ray, out hit, out on );
}
public float Headroom( ArchPlan plan, ArchKit kit, int level, IReadOnlyList<Vector2> footprint, float head )
{
var bite = ArchContact.Bite( kit );
foreach ( var headroom in AsksAll<IArchHeadroom>() )
{
if ( headroom.Over( plan, kit, level, footprint, out var top ) )
{
head = MathF.Max( head, top + bite );
}
}
return head;
}
void Answers<T>( T answer ) where T : class
{
answers[typeof( T )] = answer;
}
void AllAnswer<T>( params T[] found ) where T : class
{
everyone[typeof( T )] = found.Cast<object>().ToList();
}
}
// The same answers for a caller holding no table of its own - a handle, an overlay, a placement. Load builds all
// twelve, so a loop reaching through here builds them per row; hoist one Load into a local instead.
public static class ArchAsks
{
public static float Lift( ArchPlan plan, ArchBuilding building, ArchKit kit ) => ArchAnswers.Load().Lift( plan, building, kit );
public static IEnumerable<ArchStorey> Storeys( ArchBuilding building, ArchKit kit, ArchStyle style ) => ArchAnswers.Load().Storeys( building, kit, style );
public static bool IsWalkway( ArchRoom room ) => ArchAnswers.Load().IsWalkway( room );
public static bool Rising( ArchRoom room ) => ArchAnswers.Load().Rising( room );
public static bool WalkwayRunsAlongX( ArchRoom room ) => ArchAnswers.Load().WalkwayRunsAlongX( room );
public static IEnumerable<Vector3> PillarPositions( ArchPillarPart pillar ) => ArchAnswers.Load().PillarPositions( pillar );
public static float PillarHeight( ArchPillarPart pillar, ArchRoom room, ArchKit kit, ArchPlan plan = null )
=> ArchAnswers.Load().PillarHeight( pillar, room, kit, plan );
public static int PillarRunCount( ArchPillarPart pillar ) => ArchAnswers.Load().PillarRunCount( pillar );
public static int PillarCarriedCount( ArchPillarPart pillar ) => ArchAnswers.Load().PillarCarriedCount( pillar );
public static void StandPillar( ArchMesh canvas, ArchPillarPart pillar, ArchBrush shaft, ArchBrush end )
=> ArchAnswers.Load().StandPillar( canvas, pillar, shaft, end );
public static List<ArchPillarType> PillarTypes() => ArchShelved.Authors<ArchPillarType>( ArchShelf.Pillars );
public static bool SavePillarType( ArchPillarType type ) => ArchShelved.Save( ArchShelf.Pillars, type );
public static void PillarKinds( ToolSidebarWidget panel, IReadOnlyList<ArchPillarType> offered, string chosen, Action<ArchPillarType> adopt )
=> ArchAnswers.Load().PillarKinds( panel, offered, chosen, adopt );
public static void BuildPillar( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
=> ArchAnswers.Load().BuildPillar( panel, pillar, type, refresh, changed );
public static void SizePillar( ToolSidebarWidget panel, ArchPillarPart pillar, ArchPillarType type, Action refresh, Action changed )
=> ArchAnswers.Load().SizePillar( panel, pillar, type, refresh, changed );
public static ArchPillarPart RetypePillar( ArchTool tool, ArchPillarPart pillar, ArchPillarType type )
=> ArchAnswers.Load().RetypePillar( tool, pillar, type );
public static ArchDownpipePart PlaceDownpipe(
ArchPlan plan,
ArchBuilding building,
ArchKit kit,
Vector2 point,
string profile,
bool shoe,
PipeRun run )
=> ArchAnswers.Load().PlaceDownpipe( plan, building, kit, point, profile, shoe, run );
public static float RoofSeat( ArchRoofPart roof, ArchKit kit, Vector2 at ) => ArchAnswers.Load().RoofSeat( roof, kit, at );
public static bool RoofStruck( ArchPlan plan, int level, ArchKit kit, Ray ray, out Vector3 hit, out ArchRoofPart on )
=> ArchAnswers.Load().RoofStruck( plan, level, kit, ray, out hit, out on );
public static float Headroom( ArchPlan plan, ArchKit kit, int level, IReadOnlyList<Vector2> footprint, float head )
{
return ArchAnswers.Load().Headroom( plan, kit, level, footprint, head );
}
}