Static editor service for the advanced mapping tools. Builds part plans from generators and settings, creates generated roots in the active scene, regenerates existing objects with overrides, stores per part seed and variant overrides plus a short edit history, and wraps changes in undo scopes.
using System.Text.Json;
namespace AdvancedMappingTools;
/// <summary>
/// The operations on generated objects. Panels and tools call these, none of them touches meshes.
/// </summary>
public static class PartService
{
const int HistoryLines = 32;
/// <summary>
/// A component from a code folder added while the editor runs is not saved until the
/// project was opened again. Everything about parts depends on it being saved.
/// </summary>
static bool _canStoreParts;
public static bool CanStoreParts
{
get
{
if ( _canStoreParts ) return true;
// The first look may come before the type library has scanned the project; force that
// scan once instead of reporting a false negative and placing nothing.
_ = TypeLibrary.GetTypes<Component>().Count();
_canStoreParts = TypeLibrary.GetType( typeof( AmtPart ) ) is not null;
return _canStoreParts;
}
}
/// <summary>
/// The plan for an object with these settings and the given hand-made values.
/// </summary>
public static PartPlan Plan( IPartGenerator generator, GeneratorSettings settings, IReadOnlyDictionary<string, PartOverride> overrides = null, SurfaceBuildContext surface = null )
{
var context = new GenContext( settings.Seeds, overrides )
{
Surface = surface
};
var plan = generator.Generate( settings, context );
AnchorIntegration.Append( plan, settings, context );
foreach ( var part in plan.All() )
part.UsesSeed = context.SeedUsers.Contains( part.Id );
return plan;
}
/// <summary>
/// Place a new generated object in the active scene.
/// </summary>
public static GameObject Create( IPartGenerator generator, GeneratorSettings settings, Transform world )
{
var root = new GameObject( true, generator.Title );
root.WorldTransform = world;
root.IsStatic = true;
try
{
var plan = Plan( generator, settings, surface: SurfaceIntegration.Context( settings, root.Scene, world, root ) );
var part = root.Components.Create<AmtPart>();
part.PartId = plan.Id;
part.Generator = generator.Id;
part.Kind = plan.Kind;
part.Category = plan.Category;
part.Settings = GeneratorRegistry.Save( settings );
PartReconciler.Run( root, plan, RegenerationOptions.Everything, true );
SetGenerator( root, generator.Id );
}
catch
{
var scene = root.Scene;
root.Destroy();
scene?.ProcessDeletes();
throw;
}
return root;
}
static void SetGenerator( GameObject root, string id )
{
foreach ( var part in PartReconciler.Collect( root ) )
part.Generator = id;
}
/// <summary>
/// The root of the generated object a game object belongs to.
/// </summary>
public static GameObject RootOf( GameObject go )
{
while ( go.IsValid() )
{
if ( !string.IsNullOrEmpty( go.Components.Get<AmtPart>( FindMode.EverythingInSelf )?.Settings ) ) return go;
go = go.Parent;
}
return null;
}
public static (IPartGenerator Generator, GeneratorSettings Settings) Read( GameObject root )
{
var part = PartReconciler.RootPart( root );
var generator = GeneratorRegistry.Find( part.Generator );
return generator is null ? default : (generator, GeneratorRegistry.Load( generator, part.Settings ));
}
/// <summary>
/// Compare with or apply new settings. Without settings the stored ones are used.
/// </summary>
public static RegenerationReport Regenerate( GameObject root, RegenerationOptions options, bool apply, GeneratorSettings settings = null )
{
var(generator, stored) = Read( root );
if ( generator is null )
{
var failed = new RegenerationReport();
failed.Add( root.Name, PartChange.Protected, $"generator {PartReconciler.RootPart( root ).Generator} not found" );
return failed;
}
settings ??= stored;
var plan = Plan( generator, settings, PartReconciler.ReadOverrides( root ), SurfaceIntegration.Context( settings, root.Scene, root.WorldTransform, root ) );
var report = PartReconciler.Run( root, plan, options, apply );
if ( apply )
{
PartReconciler.RootPart( root ).Settings = GeneratorRegistry.Save( settings );
SetGenerator( root, generator.Id );
}
return report;
}
static PartOverride OverrideOf( AmtPart part ) =>
string.IsNullOrEmpty( part.Overrides ) ? new PartOverride() : JsonSerializer.Deserialize<PartOverride>( part.Overrides );
/// <summary>
/// Give one part a value by hand and rebuild only that part.
/// </summary>
public static RegenerationReport SetVariant( AmtPart part, string variant )
{
var value = OverrideOf( part );
value.Variant = variant;
part.Overrides = JsonSerializer.Serialize( value );
Note( part, $"variant {variant}" );
return Regenerate( PartRootOf( part ), RegenerationOptions.ForPart( part.PartId ), true );
}
/// <summary>
/// Give one part its own seed and rebuild only that part.
/// </summary>
public static RegenerationReport SetSeed( AmtPart part, int seed )
{
var value = OverrideOf( part );
value.Seed = seed;
part.Overrides = JsonSerializer.Serialize( value );
Note( part, $"seed {seed}" );
return Regenerate( PartRootOf( part ), RegenerationOptions.ForPart( part.PartId ), true );
}
/// <summary>
/// Throw away what was done to a part by hand.
/// </summary>
public static RegenerationReport Reset( AmtPart part )
{
part.Overrides = null;
if ( part.State == PartState.Locked ) part.State = PartState.Generated;
Note( part, "reset" );
return Regenerate( PartRootOf( part ), RegenerationOptions.ResetPart( part.PartId ), true );
}
public static void SetLocked( AmtPart part, bool locked )
{
part.State = locked ? PartState.Locked : PartState.Generated;
Note( part, locked ? "locked" : "unlocked" );
}
public static void SetHidden( AmtPart part, bool hidden )
{
part.GameObject.Enabled = !hidden;
Note( part, hidden ? "hidden" : "shown" );
}
static GameObject PartRootOf( AmtPart part ) => RootOf( part.GameObject );
static void Note( AmtPart part, string text )
{
var lines = (part.History ?? "").Split( '\n', StringSplitOptions.RemoveEmptyEntries ).TakeLast( HistoryLines - 1 );
part.History = string.Join( "\n", lines.Append( $"{DateTime.Now:yyyy-MM-dd HH:mm} {text}" ) );
}
/// <summary>
/// Run a change as one undo step, if the scene belongs to an editor session.
/// </summary>
public static IReadOnlyList<GameObject> RemovedByRegeneration( GameObject root, RegenerationOptions options, GeneratorSettings settings )
{
var report = Regenerate( root, options, false, settings );
var ids = report.Entries.Where( e => e.Change == PartChange.Removed ).Select( e => e.Id ).ToHashSet();
var doomed = PartReconciler.Collect( root ).Where( p => ids.Contains( p.PartId ) ).Select( p => p.GameObject ).ToHashSet();
bool Emptied( GameObject go ) => doomed.Contains( go ) && go.Children.All( Emptied );
return doomed.Where( go => Emptied( go ) && (go.Parent is not GameObject parent || !doomed.Contains( parent ) || !Emptied( parent )) ).ToArray();
}
public static void Transaction( string title, GameObject root, Action action, IReadOnlyList<GameObject> removed = null ) => Transaction( title, [root], action, removed );
/// <summary>
/// The same for a change that reaches several objects, like walls connected to each other.
/// </summary>
public static void Transaction( string title, IReadOnlyList<GameObject> roots, Action action, IReadOnlyList<GameObject> removed = null )
{
var scene = roots[0].Scene;
var session = SceneEditorSession.Resolve( scene );
if ( session is null )
{
using ( scene.Push() ) action();
return;
}
var undo = session.UndoScope( title ).WithGameObjectChanges( roots, GameObjectUndoFlags.All ).WithGameObjectCreations();
// What goes away has to be named before: undo brings back what it saw leaving.
if ( removed?.Count > 0 ) undo = undo.WithGameObjectDestructions( removed );
using ( scene.Push() )
using ( undo.Push() )
{
action();
}
}
}