Editor/Data/ArchBlockout.cs

Editor utility that generates and applies blockout materials for architectural roles. It defines color mappings for ArchSurface roles, produces .vmat material files with a grid texture, ensures files exist in the project's assets, provides brushes and palette application helpers, and caches a grid pixmap.

File AccessExternal Download
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchBlockout
{
	public const string Folder = "materials/blockout";
	public const string GridTexture = "materials/blockout/blockout_grid.png";

	static Pixmap grid;
	static bool written;

	// Never scale per role - 512px at 128 px/m is one 4m tile.
	public static readonly IReadOnlyList<(ArchSurface Role, string Hex)> Roles = new[]
	{
		(ArchSurface.WallExterior, "e8e4dc"),
		(ArchSurface.WallInterior, "b9c6d6"),
		(ArchSurface.WallCap, "6c7a89"),
		(ArchSurface.Reveal, "7fd1c4"),
		(ArchSurface.Trim, "ffb347"),
		(ArchSurface.Sill, "ff8c42"),
		(ArchSurface.Threshold, "b4ff39"),
		(ArchSurface.Baseboard, "e03c31"),
		(ArchSurface.Floor, "8c7b6b"),
		(ArchSurface.Ceiling, "d9cbb3"),
		(ArchSurface.Roof, "4a6fa5"),
		(ArchSurface.RoofEdge, "7ba7d9"),
		(ArchSurface.Soffit, "a8c8e8"),
		(ArchSurface.Gutter, "2e8b57"),
		(ArchSurface.StairTread, "c8a02c"),
		(ArchSurface.StairRiser, "8f6b1f"),
		(ArchSurface.StairStringer, "5e4b22"),
		(ArchSurface.Pillar, "9b59b6"),
		(ArchSurface.PillarCap, "c39bd3"),
		(ArchSurface.DoorLeaf, "d2691e"),
		(ArchSurface.Siding, "7fb069"),
		(ArchSurface.Wainscot, "4f7942"),
		(ArchSurface.WindowFrame, "ff69b4"),
		(ArchSurface.WindowSash, "ffb6d5"),
		(ArchSurface.Deck, "b5651d"),
		(ArchSurface.Railing, "00a6a6"),
		(ArchSurface.Foundation, "555555"),
		(ArchSurface.Road, "3b3b46"),
		(ArchSurface.Pavement, "8e9aa8"),
		(ArchSurface.Kerb, "f2f2f2"),
		(ArchSurface.RoadLine, "ffe14d"),
		(ArchSurface.Panel, "a3b0a0"),
		(ArchSurface.Post, "6b705c"),
		(ArchSurface.Wire, "ff2d55"),
		(ArchSurface.RoofFrame, "8a5a2b"),
		(ArchSurface.Bridge, "6e7f8d"),
		(ArchSurface.Pier, "3f4c57"),
		(ArchSurface.Lining, "8d8578"),
		(ArchSurface.Portal, "5c5346"),
		// Must read as clearly different from WallExterior, or a band can't be judged.
		(ArchSurface.WallBase, "9c5a44"),
		(ArchSurface.Pipework, "c2453d"),
		(ArchSurface.Cabling, "3d7fc2"),
		(ArchSurface.Bracket, "d8d2c4")
	};

	public static string PathFor( ArchSurface role ) => $"{Folder}/blockout_{Slug( role )}.vmat";

	public static bool Paints( ArchSurface role ) => Roles.Any( pair => pair.Role == role );

	// What a role falls back to while nothing has been painted, so the first building placed after installing the
	// library reads as a blockout rather than as missing-material grey. Writes the vmats on the first ask.
	public static ArchBrush Brush( ArchSurface role )
	{
		if ( !written )
		{
			Ensure();
		}

		return new ArchBrush
		{
			Material = ArchStyle.Load( PathFor( role ) ),
			TexelScale = ArchMesh.TexelScale
		};
	}

	public static string Describe() => $"Every role in its own hue over one seamless grid, so a defect can be named by colour"
		+ $" rather than pointed at. The grid is a ruler too - 512px at 0.3076 units/texel is exactly 4m, heavy lines every metre"
		+ $" and light every quarter. Glass is deliberately left out: painted opaque it hides every fitting behind it.";

	// Cached for the session - the file only changes when redrawn by hand.
	public static Pixmap Grid()
	{
		if ( grid is not null )
		{
			return grid;
		}

		// This library ships the grid, so it is on the content path and never in the host game's own Assets folder.
		var file = Editor.FileSystem.Content.GetFullPath( GridTexture );

		return grid = string.IsNullOrWhiteSpace( file ) ? null : Pixmap.FromFile( file );
	}

	public static Color ColourOf( ArchSurface role )
	{
		var entry = Roles.FirstOrDefault( pair => pair.Role == role );

		return entry.Hex is null ? Color.White : Color.Parse( $"#{entry.Hex}" ) ?? Color.White;
	}

	// Glass is absent on purpose - opaque it hides every fitting behind it.
	public static int Apply( ArchPalette target )
	{
		return Apply( target, Roles.Select( pair => pair.Role ) );
	}

	public static int Apply( ArchPalette target, IEnumerable<ArchSurface> roles )
	{
		if ( target is null )
		{
			return 0;
		}

		Ensure();

		var assigned = 0;

		foreach ( var role in roles.Distinct() )
		{
			target.Set( role, PathFor( role ), ArchMesh.TexelScale );
			assigned++;
		}

		ArchStyle.InvalidateCache();
		Log.Info( $"Architecture: applied the blockout grid to {assigned} roles." );

		return assigned;
	}

	public static void Clear( ArchPalette target )
	{
		if ( target is null )
		{
			return;
		}

		foreach ( var pair in Roles )
		{
			target.Set( pair.Role, null );
		}

		ArchStyle.InvalidateCache();
	}

	public static void Ensure()
	{
		var assets = Project.Current?.GetAssetsPath();

		if ( string.IsNullOrWhiteSpace( assets ) )
		{
			return;
		}

		written = true;

		var folder = Path.Combine( assets, Folder.Replace( '/', Path.DirectorySeparatorChar ) );
		Directory.CreateDirectory( folder );

		var shader = ArchHost.Load().BlockoutShader;

		foreach ( var pair in Roles )
		{
			var file = Path.Combine( folder, $"blockout_{Slug( pair.Role )}.vmat" );

			if ( File.Exists( file ) )
			{
				continue;
			}

			File.WriteAllText( file, Definition( pair.Role, pair.Hex, shader ) );
			AssetSystem.RegisterFile( file )?.Compile( false );
		}
	}

	static string Definition( ArchSurface role, string hex, string shader )
	{
		var colour = Color.Parse( $"#{hex}" ) ?? Color.White;
		var texture = role is ArchSurface.WindowFrame or ArchSurface.WindowSash or ArchSurface.Sill
			? "[1.000000 1.000000 1.000000 0.000000]"
			: GridTexture;

		return $$"""
			// THIS FILE IS AUTO-GENERATED by ArchBlockout - edit the colour table, not this file.
			// BLOCKOUT {{role}}

			Layer0
			{
				shader "{{shader}}"

				//---- Fog ----
				g_bFogEnabled "1"

				//---- Material ----
				g_flTintColor "[{{colour.r:0.000000}} {{colour.g:0.000000}} {{colour.b:0.000000}} 0.000000]"
				TextureColor "{{texture}}"
				TextureNormal "[0.500000 0.500000 1.000000 0.000000]"
				TextureRoughness "[0.850000 0.850000 0.850000 0.000000]"
				TextureMetalness "[0.000000 0.000000 0.000000 0.000000]"
				TextureAmbientOcclusion "[1.000000 1.000000 1.000000 0.000000]"
			}
			""";
	}

	static string Slug( ArchSurface role )
	{
		var name = role.ToString();
		var text = new System.Text.StringBuilder();

		foreach ( var letter in name )
		{
			if ( char.IsUpper( letter ) && text.Length > 0 )
			{
				text.Append( '_' );
			}

			text.Append( char.ToLowerInvariant( letter ) );
		}

		return text.ToString();
	}
}