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.
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 }
};
}
}