Editor utility types and asset helpers for architectural layers. Defines enums and data classes for assemblies, assets, instances and references, and provides functions to generate filesystem paths, serialize/save building/network/assembly assets, compute origins, shift road node positions, and create slugs.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum ArchAssemblyKind
{
Group,
Walkway,
Passage,
SharedCore,
}
public enum ArchAssetKind
{
Building,
Network,
Assembly,
}
// A site-level relationship: a walkway between two buildings, a shared core, any group that must
// be authored as one thing without belonging to one owner. Children reference layers by id - the
// referenced layers keep their ownership homes, the assembly is the explicit relationship.
public sealed class ArchSiteAssembly : IArchCollides, IArchNamed
{
// A group is a scope for this too: a folder of far-off houses set to None takes every part in them with it, and a
// member with its own answer still keeps it.
public ArchCollisionMode? Collision { get; set; }
public int Id { get; set; }
public string Name { get; set; } = "Assembly";
public ArchAssemblyKind Kind { get; set; }
public List<int> Children { get; set; } = new();
public ArchPalette Palette { get; set; } = new();
// How the affector in this group merges with what it reaches. The group is the relationship,
// so the terms of the join are stated on it once rather than at each end.
public ArchMergeSpec Merge { get; set; } = new();
}
// The terms of a merge, edited from the connector that owns them.
public sealed class ArchMergeSpec
{
public bool OpenWalls { get; set; } = true;
// Zero takes the connector's own clear height.
public float Clearance { get; set; }
public bool NearEnd { get; set; } = true;
public bool FarEnd { get; set; } = true;
}
// A placed copy of a reusable authored asset: the asset is the source, this record is where it
// stands. Make Local replaces it with the expanded subtree.
public sealed class ArchAssetInstance : IArchNamed
{
public int Id { get; set; }
public string Name { get; set; } = "Instance";
public ArchAssetKind Kind { get; set; }
public string AssetPath { get; set; } = "";
public Vector2 Position { get; set; }
}
// A cross-layer anchor shown in the tree: the assembly's link to a named port on another layer.
public sealed class ArchLayerReference
{
public string SourcePort { get; set; } = "";
public int TargetId { get; set; }
public string TargetPort { get; set; } = "";
}
// Reusable authored-plan assets: a finished building, road network or assembly saved to JSON and
// placed again with fresh ids. The archetype stays a rule template; this is an exact authored plan.
public static class ArchLayerAssets
{
public const string BuildingDirectory = "arch/buildings";
public const string NetworkDirectory = "arch/infrastructure";
public const string AssemblyDirectory = "arch/assemblies";
public static string BuildingPathFor( string name ) => $"{BuildingDirectory}/{Slug( name, "building" )}.archbuilding.json";
public static string NetworkPathFor( string name ) => $"{NetworkDirectory}/{Slug( name, "network" )}.archnetwork.json";
public static string AssemblyPathFor( string name ) => $"{AssemblyDirectory}/{Slug( name, "assembly" )}.archassembly.json";
public static string SaveBuilding( ArchBuilding building, string name )
{
if ( building is null || string.IsNullOrWhiteSpace( name ) )
{
return null;
}
var origin = Origin( building );
var copy = ArchStorage.Deserialize<ArchBuilding>( ArchStorage.Serialize( building ) );
ArchCarry.Shift( copy, -origin );
var path = BuildingPathFor( name );
return ArchStorage.WriteAsset( path, ArchStorage.Serialize( copy ) ) ? path : null;
}
public static string SaveNetwork( IReadOnlyList<ArchRoadPart> roads, string name )
{
if ( roads is null || roads.Count == 0 || string.IsNullOrWhiteSpace( name ) )
{
return null;
}
var origin = roads[0].Nodes.Count > 0 ? new Vector2( roads[0].Nodes[0].Position.x, roads[0].Nodes[0].Position.y ) : Vector2.Zero;
var copy = new List<ArchRoadPart>();
foreach ( var road in roads )
{
var cloned = ArchStorage.Deserialize<ArchRoadPart>( ArchStorage.Serialize( road ) );
Shift( cloned, -origin );
copy.Add( cloned );
}
var path = NetworkPathFor( name );
return ArchStorage.WriteAsset( path, ArchStorage.Serialize( copy ) ) ? path : null;
}
public static string SaveAssembly( ArchSiteAssembly assembly, string name )
{
if ( assembly is null || string.IsNullOrWhiteSpace( name ) )
{
return null;
}
var path = AssemblyPathFor( name );
return ArchStorage.WriteAsset( path, ArchStorage.Serialize( assembly ) ) ? path : null;
}
static Vector2 Origin( ArchBuilding building )
{
return ArchHandles.Bounds( building, out var min, out _ ) ? min : Vector2.Zero;
}
static void Shift( ArchRoadPart road, Vector2 shift )
{
foreach ( var node in road.Nodes )
{
node.Position += new Vector3( shift.x, shift.y, 0f );
}
}
static string Slug( string name, string fallback )
{
var slug = new string( (name ?? "")
.Trim()
.ToLowerInvariant()
.Select( character => char.IsLetterOrDigit( character ) ? character : '_' )
.ToArray() );
while ( slug.Contains( "__" ) )
{
slug = slug.Replace( "__", "_" );
}
slug = slug.Trim( '_' );
return string.IsNullOrWhiteSpace( slug ) ? fallback : slug;
}
}