Static utility for computing stable hashes identifying architecture layers and scopes in an ArchPlan. It folds various plan elements (layers, parts, instances, assemblies, buildings, roads, platforms, pipes, brackets) into 64-bit hashes to decide layer identity, grouping, and scope keys used for rebuild gating.
using System.Collections.Generic;
using System.Linq;
namespace Sunless.Architecture;
// WHAT MAKES A LAYER THE SAME LAYER - asked here and nowhere else, the way ArchLayerOrder is the one answer about
// order. Identity is the SERIALISED form, because a fold written field by field forgets the field added next month
// and a forgotten field is a change that draws nothing.
public static class ArchLayerHash
{
public static ulong Of( object payload )
{
return payload is null ? ArchHash.Seed : ArchHash.Of( ArchStorage.Keyed( payload ) );
}
// With no group in the plan every scope is ungrouped, so its own form is already in the plan-wide term and its key
// moves whenever it does. The gate could never skip one, and asking costs a serialise per rebuild.
public static bool Worth( ArchPlan plan ) => plan is { Assemblies.Count: > 0 };
// Order-dependent, because a group is a scope over an ORDERED operation stack: the same two members swapped
// build different geometry, so they must not roll up to the same key. Nested groups fold whole.
public static ulong Group( ArchPlan plan, ArchSiteAssembly group )
{
var hash = ArchHash.Fold( ArchHash.Seed, group.Id );
hash = ArchHash.Fold( hash, Of( group ) );
foreach ( var childId in group.Children )
{
if ( ArchLayerGroups.Find( plan, childId ) is { } nested )
{
hash = ArchHash.Fold( hash, Group( plan, nested ) );
continue;
}
hash = ArchHash.Fold( hash, Of( Payload( plan, childId ) ) );
}
return hash;
}
// Everything that reaches OUT of every scope, so a scope may only be skipped when none of it moved. A platform is
// in here because a pad sets a grade lift ArchCarveHosts looks for across every building; an ungrouped layer is
// because its reach is the whole plan by definition. Conservative on purpose - folding in too much costs a
// rebuild, leaving something out costs a wrong building.
public static ulong Wide( ArchPlan plan, ArchKit kit, int terrain )
{
var hash = ArchHash.Fold( ArchHash.Seed, terrain );
hash = ArchHash.Fold( hash, Of( kit ) );
foreach ( var record in plan.Layers )
{
hash = ArchHash.Fold( hash, Of( record ) );
}
// A piece's mode lives on the plan rather than on the payload, so nothing else in here would move when
// one changes and the gate would skip the scope holding it.
foreach ( var record in plan.Parts )
{
hash = ArchHash.Fold( hash, Of( record ) );
}
foreach ( var link in plan.Links )
{
hash = ArchHash.Fold( hash, Of( link ) );
}
foreach ( var instance in plan.Instances )
{
hash = ArchHash.Fold( hash, Of( instance ) );
}
foreach ( var group in plan.Assemblies )
{
hash = ArchHash.Fold( hash, ArchHash.Fold( ArchHash.Seed, group.Id ) );
hash = ArchHash.Fold( hash, Of( group ) );
}
foreach ( var platform in plan.Buildings.SelectMany( building => building.Platforms ) )
{
hash = ArchHash.Fold( hash, Of( platform ) );
}
// A services corridor reaches out of whatever scope holds it by definition: what it steps over is every
// other corridor in the plan, so one moving inside its own group changes a run in somebody else's. Left
// out, the gate skipped that other scope and it kept a hump over a bundle that had gone.
foreach ( var run in plan.Buildings.SelectMany( building => building.Pipes ) )
{
hash = ArchHash.Fold( hash, Of( run ) );
}
// And a hanger is sized off those runs, so it moves whenever they do.
foreach ( var bracket in plan.Buildings.SelectMany( building => building.Brackets ) )
{
hash = ArchHash.Fold( hash, Of( bracket ) );
}
foreach ( var scope in plan.Buildings.Where( building => ArchLayerGroups.Holding( plan, building.Id ) is null ) )
{
hash = ArchHash.Fold( hash, Of( scope ) );
}
foreach ( var road in plan.Roads().Where( road => ArchLayerGroups.Holding( plan, road.Id ) is null ) )
{
hash = ArchHash.Fold( hash, Of( road ) );
}
return hash;
}
// One building's or one road's key: what reaches everywhere, plus the scope it stands in. Held by a group,
// that is the group's roll-up; held by none, its own form is already inside the wide term, so the two
// together say "nothing anywhere in the plan that could touch this has moved".
public static ulong Scope( ulong wide, ArchPlan plan, int itemId )
{
var hash = ArchHash.Fold( wide, itemId );
if ( ArchLayerGroups.Holding( plan, itemId ) is { } group )
{
return ArchHash.Fold( hash, Group( plan, group ) );
}
return ArchHash.Fold( hash, Of( Payload( plan, itemId ) ) );
}
// Only the roots a scope can be: a group's member list holds ids of any kind, and what is not a building or a
// road is folded through the tree instead of hunted for here.
static object Payload( ArchPlan plan, int itemId )
{
if ( plan.FindBuilding( itemId ) is { } building )
{
return building;
}
if ( plan.Roads().FirstOrDefault( road => road.Id == itemId ) is { } road )
{
return road;
}
return plan.AllRooms().FirstOrDefault( room => room.Id == itemId );
}
}