Editor/Output/ArchBuildGate.cs
using System.Collections.Generic;

namespace Sunless.Architecture;

// Which scopes this build may leave alone entirely - not generated, not finished, not touched. A scope is a
// building or a road, keyed through ArchLayerHash: the group it stands in, plus everything that reaches out of
// every group. Nothing here decides what a layer's identity IS; it only asks.
//
// Absent, every scope builds - which is what a cold build, a designer stage and a preview all want.
public sealed class ArchBuildGate {
	readonly Dictionary<int, ulong> standing;
	readonly IReadOnlySet<int> refused;
	readonly IReadOnlySet<int> rebuild;
	readonly ArchPlan plan;
	readonly ulong wide;

	public ArchBuildGate( ArchPlan plan, ArchKit kit, int terrain, Dictionary<int, ulong> standing, IReadOnlySet<int> refused,
		IReadOnlySet<int> rebuild = null ) {
		this.plan = plan;
		this.standing = standing;
		this.refused = refused;
		this.rebuild = rebuild;

		wide = ArchLayerHash.Wide( plan, kit, terrain );
	}

	public Dictionary<int, ulong> Keys { get; } = new();

	// The group roll-ups worked out so far. Every member of a group asks for the same one, and working it out is a
	// serialise of the whole group - so a plan is only asked once per group per build rather than once per member.
	readonly Dictionary<int, ulong> rolled = new();

	// Asked once per scope, and it RECORDS as it answers - the key it just computed is what the next build
	// compares against, so a scope can never be skipped twice off one stale reading.
	public bool Skips( int itemId ) {
		var key = ArchLayerHash.Scope( wide, plan, itemId, rolled );

		Keys[itemId] = key;

		if ( refused is not null && refused.Contains( itemId ) ) {
			return false;
		}

		if ( rebuild is not null && !rebuild.Contains( itemId ) ) {
			return true;
		}

		return standing is not null && standing.TryGetValue( itemId, out var held ) && held == key;
	}
}