Editor dock widget for the Advanced Mapping Tools package. Shows the selected AmtPart: name, kind, id, state, generator, seed, size and hand-edit history, plus buttons to lock/hide, reseed, regenerate, reset, swap step or glass variants, and jump the selection to its building, floor or assembly. On the root of a generated object it also draws the generator settings sheet, presets, seed locks and copy/paste of settings.
namespace AdvancedMappingTools;
/// <summary>
/// Shows the selected part: what it is, what was changed on it and what can be done with it.
/// On the root of a generated object it also holds the generator settings and seeds.
/// Open it from the View menu of the editor and dock it wherever you like.
/// </summary>
[Dock( "Editor", "Davelix Inspector", "tune" )]
public sealed class PartInspector : Widget
{
readonly Sheet _sheet;
GameObject _selected;
public PartInspector( Widget parent ) : base( parent )
{
Layout = Layout.Column();
Layout.Margin = 4;
var scroll = Layout.Add( new ScrollArea( this ), 1 );
_sheet = new Sheet( scroll );
scroll.Canvas = _sheet;
_sheet.Show( null );
}
[EditorEvent.Frame]
public void Frame()
{
var current = SceneEditorSession.Active?.Selection.OfType<GameObject>().FirstOrDefault();
if ( current == _selected ) return;
_selected = current;
_sheet.Show( current );
}
sealed class Sheet : ToolSidebarWidget
{
GameObject _go;
AmtPart _part;
GameObject _root;
GeneratorSettings _settings;
SerializedObject _serialized;
public Sheet( Widget parent ) : base( parent )
{
Layout.Margin = 0;
}
public void Show( GameObject go )
{
_go = go;
_part = go.IsValid() ? go.Components.Get<AmtPart>( FindMode.EverythingInSelf ) : null;
_root = go.IsValid() ? PartService.RootOf( go ) : null;
_settings = null;
_serialized = null;
Layout.Clear( true );
if ( _part is not null )
{
AddInfo();
AddGameTags();
AddSelect();
AddActions();
AddVariants();
}
else
{
AddGroup( "Part" ).Add( new Label( go.IsValid() ? "This object was not made by Davelix Construct." : "Nothing selected." ) );
}
if ( _root.IsValid() && _go == _root )
AddGenerator();
AddManual();
Layout.AddStretchCell();
}
void Refresh() => Show( _go.IsValid() ? _go : null );
void AddGameTags()
{
_part.GameTags ??= [];
var tags = new[] { "Breakable", "Climbable", "Cover", "Loot Spot", "Navigable" };
var group = AddGroup( "Game Tags" );
var row = group.AddRow();
var per = 2;
row.Spacing = 2;
foreach ( var tag in tags )
{
var has = _part.GameTags.Contains( tag );
// Keep the engine tag in sync: the part list is the source, the tag is what game code sees.
if ( has && !_go.Tags.Has( tag ) ) _go.Tags.Add( tag );
row.Add( new ListButton( has ? $"✓ {tag}" : tag )
{
ToolTip = "Stored on this part, game code can read it.",
Clicked = () =>
{
if ( _part.GameTags.Contains( tag ) )
{
_part.GameTags.Remove( tag );
_go.Tags.Remove( tag );
}
else
{
_part.GameTags.Add( tag );
_go.Tags.Add( tag );
}
Refresh();
}
}, 1 );
if ( --per == 0 )
{
row = group.AddRow();
row.Spacing = 2;
per = 2;
}
}
}
void AddInfo()
{
var group = AddGroup( "Part" );
var edits = PartReconciler.Detect( _part );
void Row( string text ) => group.Add( new Label( text ) );
Row( _go.Name );
Row( $"Type: {_part.Kind}" );
Row( $"Id: {_part.PartId}" );
Row( $"State: {_part.State}" );
Row( $"Generator: {(string.IsNullOrEmpty( _part.Generator ) ? "none" : _part.Generator)}" );
Row( $"Seed: {_part.Seed}" );
Row( $"Size: {_part.Dimensions}" );
if ( _part.State != PartState.ManuallyCreated && !string.IsNullOrEmpty( _part.Generator ) )
Row( edits == PartEdits.None ? "Changed by hand: nothing" : $"Changed by hand: {edits}" );
if ( !string.IsNullOrEmpty( _part.History ) )
{
var history = AddGroup( "History", collapsible: true );
foreach ( var line in _part.History.Split( '\n' ).TakeLast( 8 ) )
history.Add( new Label( line ) );
}
}
void AddSelect()
{
var group = AddGroup( "Select" );
var row = group.AddRow();
row.Spacing = 2;
void Button( string label, GameObject target )
{
var button = row.Add( new ListButton( label ) { Clicked = () => Select( target ) }, 1 );
button.Enabled = target.IsValid() && target != _go;
}
Button( "Building", _root );
Button( "Floor", Ancestor( "Floor" ) );
Button( "Assembly", Ancestor( "WindowAssembly", "DoorAssembly", "StairAssembly", "RoofAssembly", "Wall", "Slab" ) );
}
GameObject Ancestor( params string[] kinds )
{
for ( var go = _go.Parent; go.IsValid(); go = go.Parent )
{
var kind = go.Components.Get<AmtPart>( FindMode.EverythingInSelf )?.Kind;
if ( kinds.Contains( kind ) ) return go;
}
return null;
}
static void Select( GameObject go )
{
var selection = SceneEditorSession.Active.Selection;
selection.Clear();
selection.Add( go );
}
void Do( string title, Action action )
{
var scope = _root.IsValid() ? _root : _go;
PartService.Transaction( title, scope, action );
Refresh();
}
void AddActions()
{
var group = AddGroup( "This Part" );
var isLocked = _part.State == PartState.Locked;
var isHidden = !_go.Enabled;
var row = group.AddRow();
row.Spacing = 2;
row.Add( new ListButton( isLocked ? "Unlock" : "Lock", isLocked ? "lock_open" : "lock" )
{
ToolTip = "A locked part is skipped by every regeneration.",
Clicked = () => Do( isLocked ? "Unlock Part" : "Lock Part", () => PartService.SetLocked( _part, !isLocked ) )
}, 1 );
row.Add( new ListButton( isHidden ? "Show" : "Hide", isHidden ? "visibility" : "visibility_off" )
{
Clicked = () => Do( isHidden ? "Show Part" : "Hide Part", () => PartService.SetHidden( _part, !isHidden ) )
}, 1 );
if ( string.IsNullOrEmpty( _part.Generator ) || !_root.IsValid() || _go == _root ) return;
row = group.AddRow();
row.Spacing = 2;
// A button only exists where it can do something: many parts take their look from their assembly.
if ( _part.UsesSeed )
{
row.Add( new ListButton( "New Seed", "casino" )
{
ToolTip = "Give only this part a new look.",
Clicked = () => Do( "New Part Seed", () => PartService.SetSeed( _part, Random.Shared.Next( 1, int.MaxValue ) ) )
}, 1 );
}
row.Add( new ListButton( "Regenerate", "refresh" )
{
ToolTip = "Rebuild this part and what is inside it. Changes by hand stay.",
Clicked = () => Do( "Regenerate Part", () => PartService.Regenerate( _root, RegenerationOptions.ForPart( _part.PartId ), true ) )
}, 1 );
group.Add( new ListButton( "Reset To Generated", "restart_alt" )
{
ToolTip = "Throw away everything changed on this part by hand.",
Clicked = () => Do( "Reset Part", () => PartService.Reset( _part ) )
} );
}
void AddVariants()
{
if ( !_root.IsValid() ) return;
if ( _part.Kind == "Step" )
{
var group = AddGroup( "Replace Step" );
var row = group.AddRow();
row.Spacing = 2;
foreach ( var variant in Enum.GetValues<StepVariant>() )
{
row.Add( new ListButton( variant.ToString() )
{
Clicked = () => Do( $"Step {variant}", () => PartService.SetVariant( _part, variant.ToString() ) )
}, 1 );
}
}
if ( _part.Kind == "Glass" )
{
var group = AddGroup( "Glass" );
var row = group.AddRow();
row.Spacing = 2;
foreach ( var variant in new[] { FrameGenerator.Intact, FrameGenerator.Broken } )
{
row.Add( new ListButton( variant )
{
Clicked = () => Do( $"Glass {variant}", () => PartService.SetVariant( _part, variant ) )
}, 1 );
}
}
}
/// <summary>
/// Show what a regeneration would do, and only do it once it is confirmed.
/// </summary>
void Regenerate( string title, RegenerationOptions options )
{
var settings = _settings;
var root = _root;
var report = PartService.Regenerate( root, options, false, settings );
Dialog.AskConfirm( () =>
{
if ( PartReconciler.RootPart( root ).Generator is RoadGenerator.GeneratorId or JunctionGenerator.GeneratorId )
RegenerateRoad( title, root, options, settings );
else
PartService.Transaction( title, root, () => PartService.Regenerate( root, options, true, settings ), PartService.RemovedByRegeneration( root, options, settings ) );
root.Scene.ProcessDeletes();
Refresh();
}, report.Summary(), title, "Apply", "Cancel" );
}
void Reconnect()
{
var walls = WallLinker.Network( _root );
var scene = _root.Scene;
if ( _settings is not null )
PartService.Regenerate( _root, RegenerationOptions.Everything, true, _settings );
PartService.Transaction( "Reconnect Walls", walls, () => WallLinker.Reconnect( scene ) );
Refresh();
}
/// <summary>
/// A road that changes takes its junctions along, as one undo step. What the change would take
/// away has to be known before, so the new settings are looked at for a moment.
/// </summary>
static void RegenerateRoad( string title, GameObject root, RegenerationOptions options, GeneratorSettings settings )
{
var scene = root.Scene;
var part = PartReconciler.RootPart( root );
var stored = part.Settings;
part.Settings = GeneratorRegistry.Save( settings );
var removed = RoadLinker.Removed( scene );
part.Settings = stored;
PartService.Transaction( title, [.. RoadLinker.Network( scene ).Except( removed )], () =>
{
PartService.Regenerate( root, options, true, settings );
RoadLinker.Reconnect( scene );
}, removed );
}
void ReconnectRoads()
{
var scene = _root.Scene;
if ( _settings is not null )
PartService.Regenerate( _root, RegenerationOptions.Everything, true, _settings );
var removed = RoadLinker.Removed( scene );
PartService.Transaction( "Reconnect Roads", [.. RoadLinker.Network( scene ).Except( removed )], () => RoadLinker.Reconnect( scene ), removed );
Refresh();
}
void AddGenerator()
{
var (generator, settings) = PartService.Read( _root );
if ( generator is null )
{
AddGroup( "Generator" ).Add( new Label( $"Generator {_part.Generator} was not found." ) );
return;
}
_settings = settings;
_serialized = settings.GetSerialized();
var actions = AddGroup( "Regenerate" );
actions.Add( new ListButton( "Preview And Apply", "preview" )
{
ToolTip = "Shows what would change first. Changes by hand and locked parts stay.",
Clicked = () => Regenerate( "Regenerate", RegenerationOptions.Everything )
} );
actions.Add( new ListButton( "Reset Settings", "restart_alt" )
{
ToolTip = "Sets every generator option and seed back to its default value.",
Clicked = () =>
{
if ( System.Activator.CreateInstance( settings.GetType() ) is not GeneratorSettings defaults ) return;
_settings = GeneratorRegistry.Merge( generator, _settings, defaults );
_serialized = _settings.GetSerialized();
Regenerate( "Reset Settings", RegenerationOptions.Everything );
}
} );
actions.Add( new ListButton( "Bring Back Deleted Parts", "undo" )
{
Clicked = () => Regenerate( "Restore Deleted Parts", new RegenerationOptions { RestoreDeleted = true } )
} );
if ( generator is ConnectedWallGenerator )
{
actions.Add( new ListButton( "Reconnect Walls", "hub" )
{
ToolTip = "Work out all wall connections again, after moving walls or changing their thickness.",
Clicked = () => Reconnect()
} );
}
if ( generator is RoadGenerator or JunctionGenerator )
{
actions.Add( new ListButton( "Reconnect Roads", "hub" )
{
ToolTip = "Work out all junctions again, after moving, deleting or changing roads.",
Clicked = () => ReconnectRoads()
} );
}
var presets = AddGroup( "Presets", collapsible: true );
presets.Add( new ListButton( "Save Preset", "save" )
{
ToolTip = "Stores the current options as a named JSON preset for this generator.",
Clicked = () =>
{
SettingsPresetService.Save( generator.Id, SettingsPresetService.NextName( generator.Id ), GeneratorRegistry.Save( settings ) );
Refresh();
}
} );
foreach ( var name in SettingsPresetService.List( generator.Id ) )
{
var presetRow = presets.AddRow();
presetRow.Spacing = 2;
presetRow.Add( new ListButton( name, "folder_open" )
{
ToolTip = "Load this preset and regenerate.",
Clicked = () =>
{
var loaded = SettingsPresetService.Load( generator, name );
if ( loaded is null ) return;
_settings = GeneratorRegistry.Merge( generator, _settings, loaded );
_serialized = _settings.GetSerialized();
Regenerate( $"Preset {name}", RegenerationOptions.Everything );
}
}, 1 );
presetRow.Add( new IconButton( "delete", () =>
{
SettingsPresetService.Delete( generator.Id, name );
Refresh();
} )
{ ToolTip = "Delete preset" } );
}
var clip = AddGroup( "Copy Settings" );
var clipRow = clip.AddRow();
clipRow.Spacing = 2;
clipRow.Add( new ListButton( "Copy", "content_copy" )
{
ToolTip = "Copy all generator options and seeds, to paste them on another object.",
Clicked = () => SettingsClipboard.Copy( generator, settings )
}, 1 );
clipRow.Add( new ListButton( "Paste", "content_paste" )
{
ToolTip = "Paste the copied options onto this object. Only works for the same generator.",
Clicked = () =>
{
if ( !SettingsClipboard.Matches( generator ) )
{
Log.Warning( "Davelix Construct: the copied settings belong to another generator." );
return;
}
var loaded = SettingsClipboard.Load( generator );
if ( loaded is null ) return;
_settings = GeneratorRegistry.Merge( generator, _settings, loaded );
_serialized = _settings.GetSerialized();
Regenerate( "Paste Settings", RegenerationOptions.Everything );
}
}, 1 );
AddSeeds( generator, settings );
PropertySheet.Fill( this, _serialized, "Settings", settings.UnusedOptions );
}
void AddSeeds( IPartGenerator generator, GeneratorSettings settings )
{
var group = AddGroup( "Seeds" );
group.Add( new ListButton( "Randomize All", "casino" )
{
ToolTip = "Locked seeds keep their value.",
Clicked = () =>
{
settings.Seeds.RandomizeAll( generator.SeedNames );
Regenerate( "Randomize All", RegenerationOptions.Everything );
}
} );
foreach ( var name in generator.SeedNames )
{
var row = group.AddRow();
row.Spacing = 2;
row.Add( new Label( $"{name}: {settings.Seeds.For( name )}" ), 1 );
row.Add( new IconButton( settings.Seeds.IsLocked( name ) ? "lock" : "lock_open", () =>
{
settings.Seeds.SetLocked( name, !settings.Seeds.IsLocked( name ) );
PartService.Transaction( "Lock Seed", _root, () => PartReconciler.RootPart( _root ).Settings = GeneratorRegistry.Save( settings ) );
Refresh();
} )
{ ToolTip = "A locked seed stays when randomizing." } );
row.Add( new IconButton( "casino", () =>
{
if ( settings.Seeds.Randomize( name ) )
Regenerate( $"Randomize {name}", RegenerationOptions.Everything );
} )
{ ToolTip = $"New seed for {name}, nothing else changes." } );
}
}
void AddManual()
{
var group = AddGroup( "Create By Hand", collapsible: true );
var parent = _go;
var row = group.AddRow();
var count = 0;
row.Spacing = 2;
foreach ( var shape in Enum.GetValues<ManualShape>() )
{
if ( count++ == 3 )
{
row = group.AddRow();
row.Spacing = 2;
}
row.Add( new ListButton( shape.ToString() )
{
ToolTip = "Regeneration never touches parts made by hand.",
Clicked = () => CreateManual( shape, parent )
}, 1 );
}
}
static void CreateManual( ManualShape shape, GameObject parent )
{
var session = SceneEditorSession.Active;
var world = parent.IsValid() ? parent.WorldTransform : new Transform( Vector3.Zero, Rotation.Identity );
using ( session.Scene.Push() )
using ( session.UndoScope( $"Create {shape}" ).WithGameObjectCreations().Push() )
{
Select( ManualParts.Create( shape, new Vector3( 32, 32, 32 ), Material.Load( GenContext.DefaultMaterial ), parent, world ) );
}
}
}
}