Editor utility for resolving and applying collision modes for architectural parts. It decides a part's collision mode from explicit settings, inherited layer/kit defaults, or canvas data, writes engine MeshComponent collision types, and creates/removes HullCollider primitives for "Solids" mode.
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public enum ArchCollisionMode
{
// Nothing to collide with at all. A silhouette on the skyline, a fitting no player reaches.
None,
// The generator's own convex pieces, as engine primitives. The cheapest answer that is still correct - and the
// only one that keeps a colonnade walkable, because each column is its own shape.
Solids,
// One convex hull over the whole part. Fills every hole in it, which is right for a lump and wrong for anything
// with a doorway or a gap the player goes through.
Convex,
// The exact concave mesh, triangle for triangle. Correct everywhere and the most expensive thing the solver has.
Complex
}
// A part that carries its own answer, overriding the kit. Authored per part because ONE wall in a plan can need
// exact collision without dragging every other part in the scene onto a concave mesh.
public interface IArchCollides
{
ArchCollisionMode? Collision { get; set; }
}
// WHAT A PART COLLIDES AS - asked here and nowhere else. The mode is resolved once, folded into the part's key so a
// toggle actually re-cooks, and written as engine components in one place.
//
// The engine constrains this more than it looks: PolygonMesh.Rebuild always cooks a hull, a collision mesh and a
// trace mesh whatever the setting, so MeshComponent.Collision buys nothing at BUILD time - it only decides which
// shapes reach the physics body. What Solids buys is the runtime: convex shapes instead of a concave mesh.
public static class ArchCollision
{
// A CHILD FOLLOWS ITS PARENT until it is given an answer of its own. The chain is the layer stack the author sees:
// the part, then every layer above it, then the unit or room a piece with no layer of its own was drawn under,
// then the kit. Overriding a layer sets that layer and everything under it still following.
public static ArchCollisionMode Resolve( ArchBuiltPart part, ArchKit kit, ArchLayerTree layers )
{
var wanted = part.Asked ?? Inherited( part.Source, layers ) ?? part.Scope ?? kit?.Physics ?? ArchCollisionMode.Solids;
if ( wanted != ArchCollisionMode.Solids )
{
return wanted;
}
// Asked for solids and the generator described the whole part with them - the only case where dropping the
// concave mesh cannot lose collision.
if ( part.Canvas.Describes && part.Canvas.Solids.Any( solid => solid.IsUsable ) )
{
return ArchCollisionMode.Solids;
}
// Described in part or not at all, so the solids are not the piece. Convex would fill a doorway and Solids
// would leave a hole to fall through; the honest fallback is the mesh the part already cooked.
return ArchCollisionMode.Complex;
}
static ArchCollisionMode? Inherited( object source, ArchLayerTree layers )
{
return source is not null && layers?.Find( source ) is { } node ? Above( node ) : null;
}
// The nearest ancestor in the stack that has been given an answer. Nothing set anywhere above means "not answered
// here" rather than a default, so the caller carries on down its own chain.
public static ArchCollisionMode? Above( ArchLayerNode node )
{
for ( var above = node?.Parent; above is not null; above = above.Parent )
{
if ( above.Payload is IArchCollides { Collision: { } inherited } )
{
return inherited;
}
}
return null;
}
// What a row shows and what its menu says it is following - one answer, so the badge and the menu cannot disagree
// about whether this layer owns its mode.
public static ArchCollisionMode Showing( ArchLayerNode node, ArchKit kit, out bool own )
{
var mine = (node?.Payload as IArchCollides)?.Collision;
own = mine.HasValue;
return mine ?? Above( node ) ?? kit?.Physics ?? ArchCollisionMode.Solids;
}
public static void Write( GameObject node, MeshComponent renderer, ArchMesh canvas, ArchCollisionMode mode )
{
renderer.Collision = mode switch
{
ArchCollisionMode.Complex => MeshComponent.CollisionType.Mesh,
ArchCollisionMode.Convex => MeshComponent.CollisionType.Hull,
_ => MeshComponent.CollisionType.None
};
Clear( node );
if ( mode != ArchCollisionMode.Solids )
{
return;
}
foreach ( var solid in canvas.Solids.Where( solid => solid.IsUsable ) )
{
Shape( node, solid );
}
}
// Rebuilt, never appended to: a part that came back with fewer pieces than last time would otherwise keep the
// shapes of the ones that are gone, and the player would walk into a column that is no longer there.
static void Clear( GameObject node )
{
foreach ( var collider in node.Components.GetAll<HullCollider>( FindMode.EverythingInSelf ).ToList() )
{
collider.Destroy();
}
}
static void Shape( GameObject node, ArchSolid solid )
{
var collider = node.Components.Create<HullCollider>();
switch ( solid.Form )
{
case ArchSolidForm.Box:
collider.Type = HullCollider.PrimitiveType.Box;
collider.Center = solid.Centre;
collider.BoxSize = solid.Size;
break;
case ArchSolidForm.Cylinder:
collider.Type = HullCollider.PrimitiveType.Cylinder;
collider.Center = solid.Centre;
collider.Radius = solid.Radius;
collider.Height = solid.Height;
collider.Slices = solid.Slices;
break;
default:
collider.Type = HullCollider.PrimitiveType.Points;
collider.Points = solid.Points.ToList();
break;
}
}
public static string Describe( ArchCollisionMode mode ) => mode switch
{
ArchCollisionMode.None => "No collision — nothing to walk into",
ArchCollisionMode.Solids => "Convex pieces — one shape per solid the generator made",
ArchCollisionMode.Convex => "One hull round the whole part — fills its holes",
_ => "Exact mesh — every triangle, the expensive one"
};
// Proven names only - the editor ships an older icon set than the web font, and one it does not know draws nothing
// at all rather than falling back. Many boxes for solids, one box round the lot for a hull.
public static string Glyph( ArchCollisionMode mode ) => mode switch
{
ArchCollisionMode.None => "block",
ArchCollisionMode.Solids => "view_module",
ArchCollisionMode.Convex => "crop_square",
_ => "grain"
};
}