Editor/Data/ArchStyle.cs

Editor-side class that resolves materials for architectural surfaces. It chooses brushes from provided palettes, falls back through role chains, caches loaded Material instances, and normalizes material paths.

File AccessReflection
using System.Collections.Generic;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchStyle
{
	static readonly Dictionary<string, Material> Cache = new( System.StringComparer.OrdinalIgnoreCase );

	readonly ArchKit kit;

	public ArchStyle( ArchKit kit )
	{
		this.kit = kit;
	}

	public static void InvalidateCache() => Cache.Clear();

	// Glass can't come from a scan library, so it has a built-in.
	const string GlassMaterial = "materials/light/glass.vmat";

	// One resolver, and the short chains get their own overloads so the common ask allocates no array at all -
	// a brush is looked up per face group of every part of every build.
	public ArchBrush Brush( ArchSurface surface ) => Resolved( surface, null, null, null );

	public ArchBrush Brush( ArchSurface surface, ArchPalette one ) => Resolved( surface, one, null, null );

	public ArchBrush Brush( ArchSurface surface, ArchPalette one, ArchPalette two ) => Resolved( surface, one, two, null );

	public ArchBrush Brush( ArchSurface surface, params ArchPalette[] chain ) => Resolved( surface, null, null, chain );

	ArchBrush Resolved( ArchSurface surface, ArchPalette one, ArchPalette two, ArchPalette[] chain )
	{
		foreach ( var role in Fallbacks( surface ) )
		{
			if ( Taken( one, role, out var first ) )
			{
				return first;
			}

			if ( Taken( two, role, out var second ) )
			{
				return second;
			}

			if ( chain is not null )
			{
				foreach ( var palette in chain )
				{
					if ( Taken( palette, role, out var found ) )
					{
						return found;
					}
				}
			}

			if ( Taken( kit?.Palette, role, out var fallback ) )
			{
				return fallback;
			}
		}

		if ( surface == ArchSurface.Glass )
		{
			return new ArchBrush { Material = Load( GlassMaterial ) };
		}

		// Nothing painted this role, so the blockout stands in for it - down the same fallback chain, because a
		// role the grid has no hue for still reads through the one it borrows from.
		foreach ( var role in Fallbacks( surface ) )
		{
			if ( ArchBlockout.Paints( role ) )
			{
				return ArchBlockout.Brush( role );
			}
		}

		return new ArchBrush { Material = ArchBrush.Missing };
	}

	bool Taken( ArchPalette palette, ArchSurface role, out ArchBrush brush )
	{
		brush = default;

		if ( palette is null || !palette.TryGet( role, out var path ) )
		{
			return false;
		}

		brush = new ArchBrush
		{
			Material = Load( path ),
			TexelScale = palette.ScaleFor( role ),
			Shift = palette.OffsetFor( role )
		};

		return true;
	}

	public static Material Load( string path )
	{
		if ( string.IsNullOrWhiteSpace( path ) )
		{
			return ArchBrush.Missing;
		}

		var normalized = Normalize( path );

		if ( Cache.TryGetValue( normalized, out var cached ) )
		{
			return cached;
		}

		var material = Material.Load( normalized ) ?? ArchBrush.Missing;
		Cache[normalized] = material;

		return material;
	}

	static string Normalize( string path )
	{
		path = path.Trim().Replace( '\\', '/' ).TrimStart( '/' );

		if ( path.StartsWith( "assets/", System.StringComparison.OrdinalIgnoreCase ) )
		{
			path = path[7..];
		}

		return path.EndsWith( ".vmat_c", System.StringComparison.OrdinalIgnoreCase ) ? path[..^2] : path;
	}

	// Built once. Handing back a fresh array per lookup allocated one per face group of every part.
	static readonly Dictionary<ArchSurface, ArchSurface[]> chains = Chains();

	// Topped up on a miss rather than indexed. ArchSurface is append-only and hotload carries this table over, so
	// a role added in the same edit is absent from it until the editor restarts - and an absent role threw where
	// it should simply have skinned itself.
	static ArchSurface[] Fallbacks( ArchSurface surface )
	{
		if ( chains.TryGetValue( surface, out var chain ) )
		{
			return chain;
		}

		return chains[surface] = Chain( surface );
	}

	static Dictionary<ArchSurface, ArchSurface[]> Chains()
	{
		var built = new Dictionary<ArchSurface, ArchSurface[]>();

		foreach ( var surface in System.Enum.GetValues<ArchSurface>() )
		{
			built[surface] = Chain( surface );
		}

		return built;
	}

	static ArchSurface[] Chain( ArchSurface surface )
	{
		return surface switch
		{
			ArchSurface.WallInterior => new[] { ArchSurface.WallInterior, ArchSurface.WallExterior },
			ArchSurface.Reveal => new[] { ArchSurface.Reveal, ArchSurface.WallInterior, ArchSurface.WallExterior },
			ArchSurface.WallCap => new[] { ArchSurface.WallCap, ArchSurface.Trim, ArchSurface.WallExterior },
			ArchSurface.Sill => new[] { ArchSurface.Sill, ArchSurface.Trim },
			ArchSurface.Threshold => new[] { ArchSurface.Threshold, ArchSurface.Sill, ArchSurface.Trim },
			ArchSurface.Baseboard => new[] { ArchSurface.Baseboard, ArchSurface.Trim },
			ArchSurface.Foundation => new[] { ArchSurface.Foundation, ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.Gutter => new[] { ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.RoofEdge => new[] { ArchSurface.RoofEdge, ArchSurface.Trim, ArchSurface.Roof },
			ArchSurface.RoofFrame => new[] { ArchSurface.RoofFrame, ArchSurface.Railing, ArchSurface.Trim },
			ArchSurface.Soffit => new[] { ArchSurface.Soffit, ArchSurface.RoofEdge, ArchSurface.Trim },
			ArchSurface.Ceiling => new[] { ArchSurface.Ceiling, ArchSurface.Floor },
			ArchSurface.PillarCap => new[] { ArchSurface.PillarCap, ArchSurface.Trim, ArchSurface.Pillar },
			ArchSurface.Pillar => new[] { ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.StairRiser => new[] { ArchSurface.StairRiser, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.StairStringer => new[] { ArchSurface.StairStringer, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.StairTread => new[] { ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.DoorLeaf => new[] { ArchSurface.DoorLeaf, ArchSurface.Trim },
			ArchSurface.Siding => new[] { ArchSurface.Siding, ArchSurface.WallExterior },
			ArchSurface.Wainscot => new[] { ArchSurface.Wainscot, ArchSurface.Trim, ArchSurface.WallInterior },
			ArchSurface.WindowFrame => new[] { ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.WindowSash => new[] { ArchSurface.WindowSash, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Deck => new[] { ArchSurface.Deck, ArchSurface.StairTread, ArchSurface.Floor },
			ArchSurface.Railing => new[] { ArchSurface.Railing, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Road => new[] { ArchSurface.Road, ArchSurface.Floor },
			ArchSurface.Pavement => new[] { ArchSurface.Pavement, ArchSurface.Deck, ArchSurface.Road, ArchSurface.Floor },
			ArchSurface.Kerb => new[] { ArchSurface.Kerb, ArchSurface.Foundation, ArchSurface.Trim },
			ArchSurface.RoadLine => new[] { ArchSurface.RoadLine, ArchSurface.Trim },
			ArchSurface.Panel => new[] { ArchSurface.Panel, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Post => new[] { ArchSurface.Post, ArchSurface.Pillar, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Wire => new[] { ArchSurface.Wire, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Grille => new[] { ArchSurface.Grille, ArchSurface.Railing, ArchSurface.WindowFrame, ArchSurface.Trim },
			ArchSurface.Shutter => new[] { ArchSurface.Shutter, ArchSurface.DoorLeaf, ArchSurface.Panel, ArchSurface.Trim },
			ArchSurface.Sign => new[] { ArchSurface.Sign, ArchSurface.Panel, ArchSurface.Trim },
			ArchSurface.Plant => new[] { ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Foundation, ArchSurface.WallExterior },
			ArchSurface.Pipework => new[] { ArchSurface.Pipework, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Cabling => new[] { ArchSurface.Cabling, ArchSurface.Wire, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Bracket => new[] { ArchSurface.Bracket, ArchSurface.Post, ArchSurface.Plant, ArchSurface.Gutter, ArchSurface.Trim },
			ArchSurface.Bridge => new[] { ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.Pillar, ArchSurface.WallExterior },
			ArchSurface.Pier => new[] { ArchSurface.Pier, ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.Pillar },
			ArchSurface.Lining => new[] { ArchSurface.Lining, ArchSurface.Bridge, ArchSurface.Foundation, ArchSurface.WallInterior },
			ArchSurface.Portal => new[] { ArchSurface.Portal, ArchSurface.Lining, ArchSurface.Bridge, ArchSurface.Foundation },
			_ => new[] { surface }
		};
	}
}