Editor/Output/ArchAnswers.cs

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.

ReflectionFile Access
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 );
	}
}