Editor-side cache for architecture builds. Tracks previously built parts, their content keys, settled keys and spatial extents by plane, and decides which parts need re-comparing or rewriting during a new build via a contact-closure algorithm.
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// What one part of the last build came out as. Reach is a plane AND the extent it occupies in it: the plane alone is
// far too coarse to scope by, because every ground slab in a plan stands in the z=0 bucket.
sealed class ArchBuiltRecord
{
public ulong Content { get; set; }
public ulong Settled { get; set; }
public Dictionary<(long, long, long, long), BBox> Reach { get; set; } = new();
}
// Which parts a rebuild actually has to hand the engine. Everything else keeps the mesh it already has, which is
// the whole point: assigning a mesh cooks a collision hull, a physics mesh and a trace mesh and uploads two
// buffers, and a handle drag was paying that for every part in the plan on every rung the mouse crossed.
public sealed class ArchBuildSettlement
{
public IReadOnlySet<int> Write { get; init; }
// Paths of parts nothing looked at this build because the scope holding them was gated out. Live, untouched,
// and the prune has to be told so - their absence from the emitted list is not a deletion.
public IReadOnlySet<string> Kept { get; init; } = new HashSet<string>();
public int Reused { get; init; }
public int Contacted { get; init; }
// A gate decision turned out to hide half of a coplanar pair from the contact pass. Nothing here is usable;
// the caller builds cold, and the scope that caused it is refused from now on.
public bool Escaped { get; init; }
}
// The bridge between two builds. Held by the tool across every regenerate and dropped whenever the standing
// geometry is not ours to reason about any more.
public sealed class ArchBuildCache
{
readonly Dictionary<string, ArchBuiltRecord> parts = new();
readonly Dictionary<(long, long, long, long), HashSet<string>> byPlane = new();
readonly Dictionary<int, ulong> scopes = new();
// Scopes that turned out to stand where something else was being re-compared, so gating them out would have
// hidden one half of a coplanar pair from the contact pass. Refused from then on, which is what stops the
// escape below from firing over and over on a plan whose houses genuinely share a wall line.
readonly HashSet<int> refused = new();
public int Count => parts.Count;
// What the last rebuild decided, so a report can say how much of the plan was actually handed to the engine.
// Verify by MEASURING: a cache nobody can read the working of is a cache nobody can tell has stopped working.
public ArchBuildSettlement Last { get; private set; }
public void Forget()
{
Restart();
refused.Clear();
}
// Everything about what is standing, but not what has been learned about which scopes may not be gated - that
// lesson is why the escape it exists for cannot loop.
public void Restart()
{
parts.Clear();
byPlane.Clear();
scopes.Clear();
}
public bool Holds( string path ) => parts.ContainsKey( path );
// The gate this build asks whether a scope can be left alone. It carries the keys it computes as it answers,
// and Settle is what commits them - so a build that threw leaves the cache saying what is actually standing.
public ArchBuildGate Gate( ArchPlan plan, ArchKit kit, int terrain )
{
return ArchLayerHash.Worth( plan ) ? new ArchBuildGate( plan, kit, terrain, scopes, refused ) : null;
}
// Finish, contact and key only what has to be. A null cache is a cold build: everything is compared and
// everything is written, which is exactly what the designers, the previews and the tests want.
public static ArchBuildSettlement Settle( ArchArchitectureBuild build, ArchBuildCache cache, ArchBuildGate gate = null )
{
if ( cache is null )
{
var all = Enumerable.Range( 0, build.Parts.Count ).ToHashSet();
new ArchMeshContactService().RemoveRedundantContacts( build.Parts );
return new ArchBuildSettlement { Write = all, Contacted = all.Count };
}
return cache.Last = cache.Settled( build, gate );
}
ArchBuildSettlement Settled( ArchArchitectureBuild build, ArchBuildGate gate )
{
var built = build.Parts;
var kept = Kept( build.Standing );
var closing = new ArchContactClosure( this );
// A part whose emitted geometry moved has to be re-compared, and so does wherever it USED to stand - the
// neighbour that was covered by a face which has now gone is uncovered and must come back.
for ( var index = 0; index < built.Count; index++ )
{
if ( parts.TryGetValue( built[index].Path, out var held ) && held.Content == built[index].Key )
{
continue;
}
closing.Enrol( built, index );
if ( held is not null )
{
closing.Sight( held.Reach );
}
}
// A part that is no longer emitted at all leaves its extent behind for the same reason - unless the scope
// holding it was gated out, in which case it is not gone, it is simply not this build's business.
var emitted = built.Select( part => part.Path ).ToHashSet();
foreach ( var gone in parts.Where( entry => !emitted.Contains( entry.Key ) && !kept.Contains( entry.Key ) ) )
{
closing.Sight( gone.Value.Reach );
}
closing.Close( built );
// A gated-out scope offered the pass no faces, so one standing where the pass is deciding leaves it half-blind
// and a covered face survives as a z-fight. Refuse it and say so: the caller builds cold, once.
if ( Escaped( kept, closing ) is { Count: > 0 } escapees )
{
foreach ( var scope in escapees )
{
refused.Add( scope );
}
return new ArchBuildSettlement { Write = new HashSet<int>(), Escaped = true };
}
new ArchMeshContactService().Resolve( closing.Faces() );
var write = new HashSet<int>();
for ( var index = 0; index < built.Count; index++ )
{
var path = built[index].Path;
var content = built[index].Key;
parts.TryGetValue( path, out var held );
// Outside the closure nothing near it moved, so its settled key is the one it already had.
var settled = closing.Holds( index ) ? Surviving( built[index], content ) : held?.Settled ?? content;
if ( held is null || held.Settled != settled )
{
write.Add( index );
}
Record( path, content, settled, closing.Reach( index ) ?? held?.Reach );
}
foreach ( var gone in parts.Keys.Where( path => !emitted.Contains( path ) && !kept.Contains( path ) ).ToList() )
{
Drop( gone );
}
foreach ( var scope in gate?.Keys ?? new Dictionary<int, ulong>() )
{
scopes[scope.Key] = scope.Value;
}
return new ArchBuildSettlement
{
Write = write,
Kept = kept,
Reused = built.Count - write.Count,
Contacted = closing.Count
};
}
internal IEnumerable<string> Standing( (long, long, long, long) plane )
{
return byPlane.TryGetValue( plane, out var held ) ? held.ToList() : Enumerable.Empty<string>();
}
internal ArchBuiltRecord Held( string path ) => parts.TryGetValue( path, out var held ) ? held : null;
// The scopes among the gated-out ones holding a part that stands where this build re-compared.
List<int> Escaped( IReadOnlySet<string> kept, ArchContactClosure closing )
{
var escapees = new List<int>();
foreach ( var path in kept )
{
if ( !parts.TryGetValue( path, out var held ) || !closing.Meets( held.Reach ) )
{
continue;
}
if ( ArchNames.TrySourceId( Segment( path ), out var scope ) )
{
escapees.Add( scope );
}
}
return escapees;
}
// Every path filed under a scope the gate left standing. Their records stay, their extents stay, and the prune
// is told to keep the nodes - none of it was this build's business.
IReadOnlySet<string> Kept( IReadOnlyList<string> segments )
{
var kept = new HashSet<string>();
if ( segments.Count == 0 )
{
return kept;
}
var standing = segments.ToHashSet();
foreach ( var path in parts.Keys )
{
if ( standing.Contains( Segment( path ) ) )
{
kept.Add( path );
}
}
return kept;
}
static string Segment( string path )
{
var end = path.IndexOf( '/' );
return end < 0 ? path : path[..end];
}
// What the contact pass LEFT: the surviving face handles, folded onto the emitted key. Two builds that emitted
// the same geometry and lost the same faces to their neighbours are the same part.
static ulong Surviving( ArchBuiltPart part, ulong content )
{
var mesh = part.Canvas.Finish();
var hash = content;
foreach ( var handle in mesh.FaceHandles )
{
hash = ArchHash.Fold( hash, handle.Index );
}
return hash;
}
void Record( string path, ulong content, ulong settled, Dictionary<(long, long, long, long), BBox> reach )
{
if ( !parts.TryGetValue( path, out var held ) )
{
held = parts[path] = new ArchBuiltRecord();
}
held.Content = content;
held.Settled = settled;
if ( reach is null || ReferenceEquals( reach, held.Reach ) )
{
return;
}
Unfile( path, held.Reach );
held.Reach = reach;
foreach ( var plane in reach.Keys )
{
(byPlane.TryGetValue( plane, out var into ) ? into : byPlane[plane] = new HashSet<string>()).Add( path );
}
}
void Drop( string path )
{
if ( !parts.TryGetValue( path, out var held ) )
{
return;
}
Unfile( path, held.Reach );
parts.Remove( path );
}
void Unfile( string path, Dictionary<(long, long, long, long), BBox> reach )
{
foreach ( var plane in reach.Keys )
{
if ( byPlane.TryGetValue( plane, out var standing ) && standing.Remove( path ) && standing.Count == 0 )
{
byPlane.Remove( plane );
}
}
}
}
// Which parts the contact pass still has to compare, closed over "stands in the same plane, near enough to touch".
// A part outside it shares no reachable plane with anything inside, so it could never have decided one of these
// faces and none of these could decide one of its - which is what makes the subset pass agree with a whole-plan one.
sealed class ArchContactClosure
{
readonly ArchBuildCache cache;
readonly Dictionary<int, List<ArchContactFace>> faces = new();
readonly Dictionary<int, Dictionary<(long, long, long, long), BBox>> reaches = new();
readonly Dictionary<(long, long, long, long), BBox> touched = new();
readonly Queue<(long, long, long, long)> frontier = new();
public ArchContactClosure( ArchBuildCache cache )
{
this.cache = cache;
}
public int Count => faces.Count;
public bool Holds( int index ) => faces.ContainsKey( index );
public Dictionary<(long, long, long, long), BBox> Reach( int index )
{
return reaches.TryGetValue( index, out var held ) ? held : null;
}
public List<ArchContactFace> Faces()
{
return faces.Keys.OrderBy( index => index ).SelectMany( index => faces[index] ).ToList();
}
public void Enrol( IReadOnlyList<ArchBuiltPart> built, int index )
{
if ( faces.ContainsKey( index ) )
{
return;
}
var collected = ArchMeshContactService.Faces( built[index], index ).ToList();
var reach = new Dictionary<(long, long, long, long), BBox>();
foreach ( var face in collected )
{
var bounds = ArchMeshContactService.Bounds( face );
reach[face.Plane] = reach.TryGetValue( face.Plane, out var held ) ? Union( held, bounds ) : bounds;
}
faces[index] = collected;
reaches[index] = reach;
Sight( reach );
}
// Growing an extent can bring in a part the smaller one did not reach, so a plane whose extent grew is queued
// again. Extents only ever grow and there are finitely many parts, so this settles.
public void Sight( Dictionary<(long, long, long, long), BBox> reach )
{
foreach ( var entry in reach )
{
if ( touched.TryGetValue( entry.Key, out var held ) )
{
var grown = Union( held, entry.Value );
if ( grown.Mins == held.Mins && grown.Maxs == held.Maxs )
{
continue;
}
touched[entry.Key] = grown;
}
else
{
touched[entry.Key] = entry.Value;
}
frontier.Enqueue( entry.Key );
}
}
public void Close( IReadOnlyList<ArchBuiltPart> built )
{
var byPath = new Dictionary<string, int>();
for ( var index = 0; index < built.Count; index++ )
{
byPath[built[index].Path] = index;
}
while ( frontier.Count > 0 )
{
var plane = frontier.Dequeue();
if ( !touched.TryGetValue( plane, out var extent ) )
{
continue;
}
foreach ( var path in cache.Standing( plane ) )
{
if ( !byPath.TryGetValue( path, out var index ) || faces.ContainsKey( index ) )
{
continue;
}
if ( cache.Held( path )?.Reach.TryGetValue( plane, out var held ) == true
&& ArchMeshContactService.Touches( extent, held ) )
{
Enrol( built, index );
}
}
}
}
// Whether a part nobody generated stands where the pass is deciding - the question the gate's escape asks.
public bool Meets( Dictionary<(long, long, long, long), BBox> reach )
{
foreach ( var entry in reach )
{
if ( touched.TryGetValue( entry.Key, out var extent ) && ArchMeshContactService.Touches( extent, entry.Value ) )
{
return true;
}
}
return false;
}
static BBox Union( BBox left, BBox right )
{
return new BBox( Vector3.Min( left.Mins, right.Mins ), Vector3.Max( left.Maxs, right.Maxs ) );
}
}