Editor utility that bakes generated architecture in a scene. It finds the generated root, merges mesh components per room into single polygon meshes, removes source objects, and can write the baked scene to a maps/raids directory via ArchStorage.
using System.Collections.Generic;
using System.Linq;
using Editor;
using MapDoor = Sandbox.Mapping.Door;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchBake
{
public static int MergeScene( Scene scene )
{
var root = ArchScene.FindRoot( scene );
if ( !root.IsValid() )
{
Log.Warning( "Architecture: nothing to bake - no generated root in this scene." );
return 0;
}
var merged = 0;
using ( SceneEditorSession.Active.UndoScope( "Bake Architecture" ).WithGameObjectChanges( root, GameObjectUndoFlags.All ).Push() )
{
foreach ( var room in Rooms( root ).ToList() )
{
merged += MergeGroup( room );
}
}
return merged;
}
public const string MapDirectory = "maps/raids";
public static void BakeToMap( Scene scene )
{
var root = ArchScene.FindRoot( scene );
if ( !root.IsValid() )
{
Log.Warning( "Architecture: nothing to bake - no generated root in this scene." );
return;
}
MergeScene( scene );
var name = System.IO.Path.GetFileNameWithoutExtension( scene.Source?.ResourcePath ?? "untitled" );
var target = $"{MapDirectory}/{name}.scene";
var file = new SceneFile { GameObjects = new[] { root.Serialize() } };
// Editor.FileSystem.Content is read-only and throws on every write, so this has to go through ArchStorage.
if ( ArchStorage.WriteAsset( target, Json.Serialize( file ) ) )
{
Log.Info( $"Architecture: wrote baked map to {target}" );
}
}
static IEnumerable<GameObject> Rooms( GameObject root )
{
foreach ( var building in root.Children )
{
foreach ( var child in building.Children )
{
yield return child;
}
}
}
static int MergeGroup( GameObject group )
{
var sources = group.Components
.GetAll<MeshComponent>( FindMode.EverythingInSelfAndDescendants )
.Where( mesh => mesh.Mesh is not null )
.Where( mesh => !HasDoorAncestor( mesh.GameObject, group ) )
.ToList();
if ( sources.Count < 2 )
{
return 0;
}
var target = new PolygonMesh();
target.SetTransform( Transform.Zero );
foreach ( var source in sources )
{
var relative = group.WorldTransform.ToLocal( source.WorldTransform );
target.MergeMesh( source.Mesh, relative, out _, out _, out _ );
}
target.SetSmoothingAngle( 0f );
var owners = sources.Select( source => source.GameObject ).Distinct().ToList();
foreach ( var source in sources )
{
source.Destroy();
}
foreach ( var owner in owners )
{
if ( owner != group && !owner.Children.Any() && !owner.Components.GetAll().Any() )
{
owner.Destroy();
}
}
var baked = group.Scene.CreateObject();
baked.Name = "Baked";
baked.SetParent( group, false );
baked.Tags.Add( ArchScene.GeneratedTag );
var renderer = baked.Components.GetOrCreate<MeshComponent>();
renderer.Mesh = target;
renderer.Color = Color.White;
renderer.SmoothingAngle = 0f;
renderer.Collision = MeshComponent.CollisionType.Mesh;
renderer.RebuildMesh();
return sources.Count;
}
static bool HasDoorAncestor( GameObject node, GameObject stopAt )
{
for ( var current = node; current.IsValid() && current != stopAt; current = current.Parent )
{
if ( current.Components.Get<MapDoor>() is not null )
{
return true;
}
}
return false;
}
}