Editor/Modules/ArchAddons.cs
namespace Sunless.Architecture;
// A whole subtool contributed by the GAME around the tool - its kind, its generator, its sheet, its widgets. The
// third host hook, and scanned for the same reason the other two are: this library cannot name a type in an
// assembly it does not reference.
//
// Everything the tool itself stands is still written out, and adding a hook here does not change that. What it
// buys is the one thing writing out cannot: a tool the game owns, hidden and shown beside the built-in ones,
// living in the game's own Editor folder where the game can edit it without touching a published package.
public interface IArchAddon {
// Named in the shelf settings dialog and in a collision report, so a clash says which addon brought it.
string Name { get; }
IEnumerable<ArchSubtoolEntry> Subtools();
// Whatever the tool's tables ask for: an IArchKindManifest, an IArchDrawer, an IArchFiler, an IArchSheet, an
// IArchHandler, an IArchExtentProvider, an IArchPicker, an IArchPreview, an IArchGuide. Each table filters this
// by the contract it holds, so an addon answers one method however many tables it reaches.
//
// Build these with plain constructors. Asking a table from in here re-enters the scan that is asking you, and
// the recursion has no floor.
IEnumerable<object> Contributes();
}
// THE ONE TABLE IN THE TOOL THAT IS HELD, and the only one that may be: every other builds per ask because hotload
// carries a static over, and this is released BY the hotload. A scan is far too expensive to run per ask - a table
// loads many times a frame while the layer stack paints - and an addon's entries are answered once, bucketed by the
// contract each table asks for, and handed back as the same arrays for the rest of the assembly's life.
//
// The pinned-assembly problem the written-out tables exist to avoid is answered by Forget: nothing enrolled here
// outlives the edit that replaced it, so the game's pre-edit types are collectable the moment they are stale.
public sealed class ArchAddons {
static volatile ArchAddons held;
[EditorEvent.Hotload]
static void Forget() {
held = null;
}
// Read through a local, the way AssetSystem's compiler cache is: an asset compile can ask off the main thread
// while a hotload on it clears the field.
public static ArchAddons Load() {
return held ?? (held = Discovered());
}
readonly IArchAddon[] enrolled;
readonly ArchSubtoolEntry[] subtools;
readonly object[] contributions;
readonly Dictionary<Type, Array> byContract = new();
ArchAddons( IArchAddon[] enrolled, ArchSubtoolEntry[] subtools, object[] contributions ) {
this.enrolled = enrolled;
this.subtools = subtools;
this.contributions = contributions;
}
public IReadOnlyList<IArchAddon> Enrolled => enrolled;
// An array, not a query: a table foreaches this on every load, and an interface handle would box the
// enumerator each time where the array's own is a struct.
public ArchSubtoolEntry[] Subtools() => subtools;
public T[] Contributed<T>() where T : class {
if ( contributions.Length == 0 ) {
return Array.Empty<T>();
}
lock ( byContract ) {
if ( byContract.TryGetValue( typeof( T ), out var bucketed ) ) {
return (T[])bucketed;
}
var bucket = contributions.OfType<T>().ToArray();
byContract[typeof( T )] = bucket;
return bucket;
}
}
static ArchAddons Discovered() {
var found = ArchDiscovery.EnrolledByName<IArchAddon>().ToArray();
if ( found.Length == 0 ) {
return new ArchAddons( Array.Empty<IArchAddon>(), Array.Empty<ArchSubtoolEntry>(), Array.Empty<object>() );
}
var shelved = new List<ArchSubtoolEntry>();
var given = new List<object>();
foreach ( var addon in found ) {
shelved.AddRange( Offered( addon ) );
given.AddRange( Given( addon ) );
}
return new ArchAddons( found, shelved.ToArray(), given.ToArray() );
}
// An addon that throws is reported and walked past, never taken down the table asking it: one broken game
// hook must not be the tool refusing to open.
static IEnumerable<ArchSubtoolEntry> Offered( IArchAddon addon ) {
try {
return addon.Subtools()?.Where( entry => entry is not null ) ?? Enumerable.Empty<ArchSubtoolEntry>();
} catch ( Exception fault ) {
Log.Warning( $"Architecture addon {addon.Name} threw while naming its subtools: {fault.Message}" );
return Enumerable.Empty<ArchSubtoolEntry>();
}
}
static IEnumerable<object> Given( IArchAddon addon ) {
try {
return addon.Contributes()?.Where( entry => entry is not null ) ?? Enumerable.Empty<object>();
} catch ( Exception fault ) {
Log.Warning( $"Architecture addon {addon.Name} threw while contributing: {fault.Message}" );
return Enumerable.Empty<object>();
}
}
}