Editor/Layers/ArchLayerGroups.cs
using System.Collections.Generic;
using System.Linq;
namespace Sunless.Architecture;
// A group is an authored assembly, and an assembly is a SCOPE: an operation inside one may only
// reach the layers that share it. A walkway grouped with the two houses it joins keeps its mouths
// open through their walls; dragged out of the group it reaches nothing and they close.
public static class ArchLayerGroups {
public static ArchSiteAssembly Holding( ArchPlan plan, int itemId ) {
if ( itemId == 0 ) {
return null;
}
return ArchBuildMemo.Held( memo => memo.Holders, itemId,
() => plan?.Assemblies.FirstOrDefault( group => group.Children.Contains( itemId ) ) );
}
public static ArchSiteAssembly Find( ArchPlan plan, int groupId ) {
return plan?.Assemblies.FirstOrDefault( group => group.Id == groupId );
}
// One namer, so a group a link formed and a group a flight formed read as the same kind of thing.
public static string Named( ArchPlan plan ) => $"Housing Group {plan.Assemblies.Count + 1}";
public static ArchSiteAssembly Create( ArchPlan plan, string name, ArchAssemblyKind kind, IEnumerable<int> members ) {
var group = new ArchSiteAssembly { Id = plan.AllocateId(), Name = name, Kind = kind };
foreach ( var member in members.Where( id => id != 0 ).Distinct() ) {
Leave( plan, member );
group.Children.Add( member );
}
plan.Assemblies.Add( group );
return group;
}
// One membership at a time - a layer joining a group leaves the one it was in, or the tree would
// show the same authored thing standing in two scopes.
public static void Join( ArchPlan plan, ArchSiteAssembly group, int itemId ) {
if ( group is null || itemId == 0 || group.Id == itemId ) {
return;
}
Leave( plan, itemId );
group.Children.Add( itemId );
}
public static void Leave( ArchPlan plan, int itemId ) {
foreach ( var group in plan?.Assemblies ?? Enumerable.Empty<ArchSiteAssembly>() ) {
group.Children.Remove( itemId );
}
}
// The group goes, its members stay - they fall back to the typed ownership they never left.
public static void Dissolve( ArchPlan plan, ArchSiteAssembly group ) {
if ( group is null ) {
return;
}
plan.Assemblies.Remove( group );
plan.Links.RemoveAll( link => link.SourceId == group.Id || link.TargetId == group.Id );
plan.Layers.RemoveAll( record => record.ItemId == group.Id );
}
// Every id inside a group, folders included: a group holding a group of houses reaches those
// houses, or nesting what an affector affects would quietly put it out of its own scope.
public static IReadOnlySet<int> Flatten( ArchPlan plan, ArchSiteAssembly group ) {
var members = new HashSet<int>();
Gather( plan, group, members );
return members;
}
static void Gather( ArchPlan plan, ArchSiteAssembly group, HashSet<int> into ) {
if ( group is null || !into.Add( group.Id ) ) {
return;
}
foreach ( var child in group.Children ) {
if ( Find( plan, child ) is { } nested ) {
Gather( plan, nested, into );
continue;
}
into.Add( child );
}
}
// Everything the layer is allowed to affect: its group's members and everything those members
// own. An ungrouped layer answers null, meaning the whole plan - the way it worked before groups.
public static IReadOnlySet<int> Scope( ArchPlan plan, ArchLayerTree tree, int itemId ) {
if ( Holding( plan, itemId ) is not { } group ) {
return null;
}
var scope = new HashSet<int>( Flatten( plan, group ) );
foreach ( var member in scope.ToList() ) {
if ( tree?.Find( member ) is { Ref: { } layer } ) {
foreach ( var descendant in tree.DirtyClosure( layer.ItemId ) ) {
scope.Add( descendant );
}
}
}
foreach ( var link in plan.Links.Where( link => link.SourceId == group.Id ) ) {
scope.Add( link.TargetId );
}
return scope;
}
// Buildings the layer may cut into. Candidates are tried in order, so an affector's own membership
// decides before the host it was filed in - which is what makes a shaft drawn inside a grouped
// house open that group's slabs and nobody else's. Ungrouped answers null: every building, as before.
public static IReadOnlySet<int> Reach( ArchPlan plan, params int[] candidates ) {
// The memo keys on the affector and its host, which is every caller; a longer candidate list would
// collide on that key, so it resolves plainly instead of holding a wrong answer.
if ( candidates.Length > 2 ) {
return Reached( plan, candidates );
}
var affector = candidates.Length > 0 ? candidates[0] : 0;
var host = candidates.Length > 1 ? candidates[1] : 0;
return ArchBuildMemo.Held( memo => memo.Reaches, (affector, host), () => Reached( plan, candidates ) );
}
static IReadOnlySet<int> Reached( ArchPlan plan, int[] candidates ) {
foreach ( var candidate in candidates ) {
if ( Holding( plan, candidate ) is not { } group ) {
continue;
}
var members = Flatten( plan, group );
var reach = new HashSet<int>();
foreach ( var building in plan.Buildings ) {
if ( members.Contains( building.Id ) || building.Rooms.Any( room => members.Contains( room.Id ) ) ) {
reach.Add( building.Id );
}
}
return reach;
}
return null;
}
public static void Link( ArchPlan plan, int sourceId, string sourcePort, int targetId, string targetPort ) {
plan.Links.RemoveAll( link => link.SourceId == sourceId && link.SourcePort == sourcePort );
if ( targetId == 0 ) {
return;
}
plan.Links.Add( new ArchLayerLink {
SourceId = sourceId,
SourcePort = sourcePort,
TargetId = targetId,
TargetPort = targetPort,
} );
}
}