Editor/Output/ArchHost.cs

Interface and runtime host for architecture-related configuration. IArchHost declares two string properties for blockout shader and material library paths. ArchHost provides defaults, loads an implementation via ArchDiscovery.EnrolledByName<IArchHost>().FirstOrDefault(), and returns an instance using provided answers or stock fallbacks.

Reflection
namespace Sunless.Architecture;

// What the tool cannot know about the game it generates into. A game answers here rather than repainting what the
// tool produced; nothing answering is the tool standing on its own, so every fact has a stock answer.
public interface IArchHost
{
	// The shader a generated blockout material is written against. A game with its own lighting model names it
	// here instead of rewriting every .vmat the tool just wrote.
	string BlockoutShader { get; }

	// Where the material browser looks for the surface library, relative to the open project's root.
	string MaterialLibrary { get; }
}

// Never held between asks, for the reason ArchDoorFitters is not: hotload carries a static over.
public sealed class ArchHost
{
	// The library's own, because complex.shader tints through g_vColorTint and every generated blockout material
	// states its colour as g_flTintColor - on the stock shader the tint was dropped and the lot rendered white.
	public const string StockShader = "shaders/arch_blockout.shader";

	public const string StockLibrary = "Assets/materials";

	public string BlockoutShader { get; private init; } = StockShader;

	public string MaterialLibrary { get; private init; } = StockLibrary;

	// Ordered by name and first answer taken, so two games open in one editor cannot make the result depend on
	// which assembly enrolled first. A fact left blank falls back rather than writing an empty shader path.
	public static ArchHost Load()
	{
		var spoken = ArchDiscovery.EnrolledByName<IArchHost>().FirstOrDefault();

		if ( spoken is null )
		{
			return new ArchHost();
		}

		return new ArchHost
		{
			BlockoutShader = Answered( spoken.BlockoutShader, StockShader ),
			MaterialLibrary = Answered( spoken.MaterialLibrary, StockLibrary )
		};
	}

	static string Answered( string answer, string stock )
	{
		return string.IsNullOrWhiteSpace( answer ) ? stock : answer;
	}
}