Editor/Layers/ArchLayerHash.cs
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.
//
// `held` is where a build remembers what it has already worked out. Every member of a group asks for the SAME
// roll-up, so without one a group of sixteen serialises seventeen payloads sixteen times over.
public static ulong Group( ArchPlan plan, ArchSiteAssembly group, Dictionary<int, ulong> held = null ) {
if ( held is not null && held.TryGetValue( group.Id, out var remembered ) ) {
return remembered;
}
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, held ) );
continue;
}
hash = ArchHash.Fold( hash, Of( Payload( plan, childId ) ) );
}
if ( held is not null ) {
held[group.Id] = hash;
}
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, Dictionary<int, ulong> held = null ) {
var hash = ArchHash.Fold( wide, itemId );
if ( ArchLayerGroups.Holding( plan, itemId ) is { } group ) {
return ArchHash.Fold( hash, Group( plan, group, held ) );
}
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 );
}
}