Editor-side utility for barrier/rail architecture placement. Defines ArchBarrierPost, shapes for barrier bays and whole barrier runs, and provides Resolve logic to sample an ArchCurve into posts and bays with procedural variation (noise, sag, seats, courses). It is used by the editor placement/ghost and generator to produce consistent barrier geometry.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchBarrierPost
{
public ArchFrame Frame { get; init; }
public float Seat { get; init; }
public float Foot { get; init; }
public float Lean { get; init; }
public bool End { get; init; }
public Vector2 Flat => new( Frame.Position.x, Frame.Position.y );
}
// Seats differ only on a raking run - a stepped run levels them.
public sealed class ArchBarrierBayShape
{
const float StrandChord = 48f;
public ArchBarrierPost From { get; init; }
public ArchBarrierPost To { get; init; }
public float SeatFrom { get; init; }
public float SeatTo { get; init; }
public float Ground { get; init; }
public int Courses { get; init; }
public bool Gate { get; init; }
public bool Missing { get; init; }
public float Lean { get; init; }
public float Slip { get; init; }
public float Sag { get; init; }
public int Index { get; init; }
public float Span => (To.Flat - From.Flat).Length;
// The one sagging line. The drop was resolved once, on the bay, so the ghost walks exactly the points the
// generator builds and neither can re-derive a curve of its own.
public List<Vector3> Strand( float from, float to )
{
var chords = Math.Max( 1, ArchDivide.AtMost( Span, StrandChord ).Count );
var path = new List<Vector3>();
var span = To.Flat - From.Flat;
for ( var chord = 0; chord <= chords; chord++ )
{
var fraction = chord / (float)chords;
var flat = From.Flat + span * fraction;
var level = MathX.Lerp( from, to, fraction );
path.Add( new Vector3( flat.x, flat.y, level - Sag * 4f * fraction * (1f - fraction) ) );
}
return path;
}
public Vector2 Along
{
get
{
var span = To.Flat - From.Flat;
return span.Length < 0.05f ? new Vector2( 1f, 0f ) : span.Normal;
}
}
public float SeatAt( float fraction ) => MathX.Lerp( SeatFrom, SeatTo, fraction );
}
// One resolve, read by the generator AND the placement ghost, so the posts drawn are the posts built.
public sealed class ArchBarrierShape
{
public List<ArchBarrierPost> Posts { get; init; } = new();
public List<ArchBarrierBayShape> Bays { get; init; } = new();
public bool IsUsable => Bays.Count > 0;
public static ArchBarrierShape Resolve( ArchCurve curve, ArchBarrierSpec spec, float offset = 0f, float lift = 0f, ArchKit kit = null )
{
var shape = new ArchBarrierShape();
if ( curve is null || !curve.IsUsable || spec is null )
{
return shape;
}
var length = curve.Length;
if ( length < 4f )
{
return shape;
}
var stations = Stations( curve, spec, length, offset, lift );
if ( stations.Count < 2 )
{
return shape;
}
var seats = Seats( stations, spec );
var gate = spec.HasGate ? (spec.GateAt, spec.GateAt + spec.GateWidth) : (0f, 0f);
for ( var index = 0; index < stations.Count; index++ )
{
var neighbours = new List<float> { seats[Math.Max( 0, index - 1 )], seats[Math.Min( seats.Count - 1, index )] };
shape.Posts.Add( new ArchBarrierPost
{
Frame = stations[index],
Seat = neighbours.Max(),
Foot = MathF.Min( stations[index].Position.z, neighbours.Min() ),
Lean = Signed( spec.Seed, index, 11 ) * spec.Wear * 2.5f,
End = !curve.Closed && (index == 0 || index == stations.Count - 1)
} );
}
for ( var index = 0; index < seats.Count; index++ )
{
var from = shape.Posts[index];
var to = shape.Posts[index + 1];
var authored = spec.Bays.ElementAtOrDefault( index );
var midpoint = (from.Frame.Distance + to.Frame.Distance) * 0.5f;
shape.Bays.Add( new ArchBarrierBayShape
{
From = from,
To = to,
SeatFrom = spec.Ground == BarrierGround.Follow ? from.Frame.Position.z : seats[index],
SeatTo = spec.Ground == BarrierGround.Follow ? to.Frame.Position.z : seats[index],
Ground = MathF.Min( from.Frame.Position.z, to.Frame.Position.z ),
Courses = Courses( spec, index, authored ),
Gate = authored?.Gate == true || (spec.HasGate && midpoint >= gate.Item1 && midpoint <= gate.Item2),
Missing = spec.Broken > 0.001f && Noise( spec.Seed, index, 23 ) < spec.Broken,
Lean = Signed( spec.Seed, index, 31 ) * spec.Wear * 3.5f,
// Down only - a panel settles; a lifted one would top the wall's height line.
Slip = -Noise( spec.Seed, index, 47 ) * spec.Wear * 2.5f,
Sag = Sagged( spec, kit, (to.Flat - from.Flat).Length, index ),
Index = index
} );
}
return shape;
}
static List<ArchFrame> Stations( ArchCurve curve, ArchBarrierSpec spec, float length, float offset, float lift )
{
var distances = Distances( spec, length );
var stations = new List<ArchFrame>();
foreach ( var distance in distances )
{
if ( !curve.Sample( Math.Clamp( distance, 0f, length ), out var frame ) )
{
continue;
}
stations.Add( offset == 0f && lift == 0f ? frame : Shifted( frame, offset, lift ) );
}
return stations;
}
static List<float> Distances( ArchBarrierSpec spec, float length )
{
var panel = MathF.Max( 12f, spec.PanelLength );
if ( spec.Bays.Count == 0 )
{
return spec.EvenBays
? ArchDivide.AtMost( length, panel ).Nodes.ToList()
: ArchDivide.Fixed( length, panel );
}
var distances = new List<float> { 0f };
var travelled = 0f;
foreach ( var bay in spec.Bays )
{
travelled += MathF.Max( 12f, bay.Span > 0.5f ? bay.Span : panel );
if ( travelled >= length - 6f )
{
break;
}
distances.Add( travelled );
}
// A run extended past its own bay list carries on at the panel length rather than ending in one enormous bay.
while ( length - travelled > panel * 1.15f )
{
travelled += panel;
distances.Add( travelled );
}
distances.Add( length );
return distances;
}
// Sits on the higher end; the plinth makes up the difference.
static List<float> Seats( IReadOnlyList<ArchFrame> stations, ArchBarrierSpec spec )
{
var seats = new List<float>();
var highest = stations.Max( station => station.Position.z );
for ( var index = 0; index < stations.Count - 1; index++ )
{
seats.Add( spec.Ground switch
{
BarrierGround.Level => highest,
BarrierGround.Step => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z ),
_ => MathF.Max( stations[index].Position.z, stations[index + 1].Position.z )
} );
}
return seats;
}
// A FRACTION of the span, so one kit number sags a short spur and a long crossing alike, and wear deepens it.
static float Sagged( ArchBarrierSpec spec, ArchKit kit, float span, int index )
{
if ( spec.Style != BarrierStyle.Catenary )
{
return 0f;
}
var fraction = MathF.Max( 0f, kit?.CatenarySag ?? 0f );
return span * fraction * (1f + Noise( spec.Seed, index, 113 ) * spec.Wear);
}
// Every emitted vertex canonicalizes onto the finest rung, so a strand thinner than TWO rungs has both
// sides of its section land on one of them and the whole prism collapses to nothing. The kit may ask for
// a hair-thin wire; the mesh cannot hold one, and a line nobody can see is worse than one a touch fat.
public static float WireHalf( ArchKit kit )
{
return MathF.Max( ArchGridService.FinestSize, (kit?.WireThickness ?? 0f) * 0.5f );
}
static int Courses( ArchBarrierSpec spec, int index, ArchBarrierBay authored )
{
if ( authored is { Courses: > 0 } )
{
return authored.Courses;
}
var courses = Math.Max( 1, spec.Courses );
if ( spec.CourseVary <= 0 )
{
return courses;
}
// Down only - Courses is the height line; a bay falls short, never over.
return Math.Max( 1, courses - (int)MathF.Round( Noise( spec.Seed, index, 59 ) * spec.CourseVary ) );
}
static ArchFrame Shifted( ArchFrame frame, float offset, float lift )
{
return new ArchFrame
{
Position = frame.Side( offset, lift ),
Along = frame.Along,
Across = frame.Across,
Up = frame.Up,
Distance = frame.Distance,
WidthScale = frame.WidthScale
};
}
// Seeded, never Game.Random: a run that reshuffled itself on every hotload could not be reviewed.
public static float Noise( int seed, int index, int salt )
{
var hash = unchecked((uint)(seed * 374761393 + index * 668265263 + salt * 2246822519));
hash ^= hash >> 13;
hash = unchecked(hash * 1274126177);
hash ^= hash >> 16;
return (hash & 0xFFFFFF) / (float)0xFFFFFF;
}
public static float Signed( int seed, int index, int salt ) => Noise( seed, index, salt ) * 2f - 1f;
}