365 lines
10 KiB
C#
365 lines
10 KiB
C#
#if TOOLS
|
|
|
|
using Godot;
|
|
using System.Collections.Generic;
|
|
|
|
namespace Rokojori.Tools
|
|
{
|
|
using Rokojori;
|
|
|
|
// Like Node.Duplicate(), but [Export] Node/array/list members that point outside the
|
|
// duplicated subtree (parent, siblings, cousins, ...) keep pointing at the original
|
|
// external target instead of being nulled out. Members pointing inside the subtree are
|
|
// remapped to the corresponding duplicated node, same as native duplicate already does.
|
|
//
|
|
// Scene instances (nodes with a SceneFilePath, like an instanced Cyli.tscn) are not
|
|
// copied node by node, they are re-instantiated from their scene with the editor edit
|
|
// state, so the copy stays an instance of that scene instead of being flattened into
|
|
// the edited scene. Node pairing is done by relative path, never by iteration index,
|
|
// because the copy's subtree can legitimately differ from the live one in the editor.
|
|
public static class NodeDuplicator
|
|
{
|
|
static readonly HashSet<string> nonCopyableProperties = new HashSet<string>()
|
|
{
|
|
"name",
|
|
"owner",
|
|
"script",
|
|
"scene_file_path",
|
|
"unique_name_in_owner",
|
|
"multiplayer"
|
|
};
|
|
|
|
public static Node DuplicateDeep( Node original )
|
|
{
|
|
if ( original == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var parent = original.GetParent();
|
|
|
|
if ( parent == null )
|
|
{
|
|
original.LogError( "Duplicate Deep needs a parent node, can't duplicate the scene root." );
|
|
return null;
|
|
}
|
|
|
|
var originalNodes = GetSubtree( original );
|
|
var duplicate = CreateCopy( original );
|
|
|
|
if ( duplicate == null )
|
|
{
|
|
return null;
|
|
}
|
|
|
|
parent.AddChild( duplicate, true );
|
|
parent.MoveChild( duplicate, original.GetIndex() + 1 );
|
|
|
|
var map = MapByPath( original, originalNodes, duplicate );
|
|
|
|
AddMissingNodes( original, originalNodes, duplicate, map );
|
|
AssignOwners( original, originalNodes, map );
|
|
RemapExternalReferences( originalNodes, map );
|
|
|
|
duplicate.LogInfo( "Duplicated", original.Name, "as", duplicate.Name );
|
|
|
|
return duplicate;
|
|
}
|
|
|
|
static List<Node> GetSubtree( Node root )
|
|
{
|
|
var nodes = new List<Node>();
|
|
NodesWalker.Get().Iterate( root, n => nodes.Add( n ), false );
|
|
return nodes;
|
|
}
|
|
|
|
static Node CreateCopy( Node original )
|
|
{
|
|
var scenePath = original.SceneFilePath;
|
|
|
|
if ( string.IsNullOrEmpty( scenePath ) )
|
|
{
|
|
return original.Duplicate( (int) Node.DuplicateFlags.Default );
|
|
}
|
|
|
|
var scene = ResourceLoader.Load<PackedScene>( scenePath );
|
|
|
|
if ( scene == null )
|
|
{
|
|
original.LogError( "Couldn't load scene for duplication:", scenePath, "using plain duplicate." );
|
|
return original.Duplicate( (int) Node.DuplicateFlags.Default );
|
|
}
|
|
|
|
var copy = scene.Instantiate<Node>( PackedScene.GenEditState.Instance );
|
|
|
|
if ( copy == null )
|
|
{
|
|
original.LogError( "Couldn't instantiate scene for duplication:", scenePath );
|
|
return null;
|
|
}
|
|
|
|
copy.Name = original.Name;
|
|
CopyStoredProperties( original, copy );
|
|
|
|
return copy;
|
|
}
|
|
|
|
// An instanced scene only stores overrides on top of its scene, so the freshly
|
|
// instantiated copy has to receive the original's own property values.
|
|
static void CopyStoredProperties( Node original, Node copy )
|
|
{
|
|
foreach ( var propertyInfo in original.GetPropertyList() )
|
|
{
|
|
var dict = (Godot.Collections.Dictionary) propertyInfo;
|
|
|
|
if ( ! IsStored( dict ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var name = dict[ "name" ].AsString();
|
|
|
|
if ( string.IsNullOrEmpty( name ) || nonCopyableProperties.Contains( name ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
copy.Set( name, original.Get( name ) );
|
|
}
|
|
}
|
|
|
|
// Pairs original nodes with copied nodes by their path relative to the subtree root.
|
|
// Nodes missing in the copy are left out instead of shifting all following pairs.
|
|
static Dictionary<Node,Node> MapByPath( Node originalRoot, List<Node> originalNodes, Node duplicateRoot )
|
|
{
|
|
var map = new Dictionary<Node,Node>();
|
|
|
|
foreach ( var node in originalNodes )
|
|
{
|
|
if ( node == originalRoot )
|
|
{
|
|
map[ node ] = duplicateRoot;
|
|
continue;
|
|
}
|
|
|
|
var counterpart = duplicateRoot.GetNodeOrNull( originalRoot.GetPathTo( node ) );
|
|
|
|
if ( counterpart == null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
map[ node ] = counterpart;
|
|
}
|
|
|
|
return map;
|
|
}
|
|
|
|
// Nodes that only exist in the live original, like children created at editor runtime
|
|
// by tool scripts or nodes added to a non editable instance, are not part of the
|
|
// re-instantiated copy. They get recreated at the same place so the copy matches the
|
|
// original and references to them can be remapped.
|
|
static void AddMissingNodes( Node originalRoot, List<Node> originalNodes, Node duplicateRoot, Dictionary<Node,Node> map )
|
|
{
|
|
var added = new List<string>();
|
|
var unresolved = new List<string>();
|
|
|
|
// originalNodes is in pre order, so a parent is always recreated before its children.
|
|
foreach ( var original in originalNodes )
|
|
{
|
|
if ( map.ContainsKey( original ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var relativePath = originalRoot.GetPathTo( original ).ToString();
|
|
var counterpart = duplicateRoot.GetNodeOrNull( relativePath );
|
|
|
|
if ( counterpart != null )
|
|
{
|
|
// Came in as a child of an already recreated node.
|
|
map[ original ] = counterpart;
|
|
continue;
|
|
}
|
|
|
|
var originalParent = original.GetParent();
|
|
|
|
if ( originalParent == null || ! map.TryGetValue( originalParent, out var duplicateParent ) )
|
|
{
|
|
unresolved.Add( relativePath );
|
|
continue;
|
|
}
|
|
|
|
var copy = CreateCopy( original );
|
|
|
|
if ( copy == null )
|
|
{
|
|
unresolved.Add( relativePath );
|
|
continue;
|
|
}
|
|
|
|
copy.Name = original.Name;
|
|
duplicateParent.AddChild( copy );
|
|
duplicateParent.MoveChild( copy, Mathf.Min( original.GetIndex(), duplicateParent.GetChildCount() - 1 ) );
|
|
|
|
map[ original ] = copy;
|
|
added.Add( relativePath );
|
|
}
|
|
|
|
if ( added.Count > 0 )
|
|
{
|
|
duplicateRoot.LogInfo(
|
|
"Recreated", added.Count, "nodes missing in the copy:", string.Join( ", ", added )
|
|
);
|
|
}
|
|
|
|
if ( unresolved.Count > 0 )
|
|
{
|
|
duplicateRoot.LogError(
|
|
"Couldn't recreate", unresolved.Count, "nodes, references to them are kept as is:",
|
|
string.Join( ", ", unresolved )
|
|
);
|
|
}
|
|
}
|
|
|
|
// Mirrors the original's ownership instead of owning everything, so the inner nodes
|
|
// of an instanced scene stay owned by their instance root.
|
|
static void AssignOwners( Node originalRoot, List<Node> originalNodes, Dictionary<Node,Node> map )
|
|
{
|
|
foreach ( var original in originalNodes )
|
|
{
|
|
if ( ! map.TryGetValue( original, out var duplicate ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var owner = original == originalRoot ? originalRoot.Owner : original.Owner;
|
|
|
|
if ( owner == null )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
duplicate.Owner = Remap( owner, map );
|
|
}
|
|
}
|
|
|
|
static void RemapExternalReferences( List<Node> originalNodes, Dictionary<Node,Node> map )
|
|
{
|
|
foreach ( var original in originalNodes )
|
|
{
|
|
if ( ! map.TryGetValue( original, out var duplicate ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
RemapNodeExports( original, duplicate, map );
|
|
}
|
|
}
|
|
|
|
static void RemapNodeExports( Node original, Node duplicate, Dictionary<Node, Node> map )
|
|
{
|
|
var properties = original.GetPropertyList();
|
|
|
|
foreach ( var propertyInfo in properties )
|
|
{
|
|
var dict = (Godot.Collections.Dictionary) propertyInfo;
|
|
|
|
if ( ! IsStored( dict ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var name = dict[ "name" ].AsString();
|
|
|
|
if ( string.IsNullOrEmpty( name ) )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var type = (Variant.Type) dict[ "type" ].AsInt32();
|
|
|
|
if ( type == Variant.Type.Object )
|
|
{
|
|
RemapSingleProperty( original, duplicate, name, map );
|
|
}
|
|
else if ( type == Variant.Type.Array )
|
|
{
|
|
RemapArrayProperty( original, duplicate, name, map );
|
|
}
|
|
}
|
|
}
|
|
|
|
static bool IsStored( Godot.Collections.Dictionary propertyInfo )
|
|
{
|
|
if ( ! propertyInfo.ContainsKey( "usage" ) )
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var usage = propertyInfo[ "usage" ].AsInt64();
|
|
return ( usage & (long) PropertyUsageFlags.Storage ) != 0;
|
|
}
|
|
|
|
static void RemapSingleProperty( Node original, Node duplicate, string name, Dictionary<Node, Node> map )
|
|
{
|
|
var value = original.Get( name );
|
|
|
|
if ( value.VariantType != Variant.Type.Object )
|
|
{
|
|
return;
|
|
}
|
|
|
|
if ( value.AsGodotObject() is not Node targetNode )
|
|
{
|
|
return;
|
|
}
|
|
|
|
duplicate.Set( name, Remap( targetNode, map ) );
|
|
}
|
|
|
|
static void RemapArrayProperty( Node original, Node duplicate, string name, Dictionary<Node, Node> map )
|
|
{
|
|
var value = original.Get( name );
|
|
|
|
if ( value.VariantType != Variant.Type.Array )
|
|
{
|
|
return;
|
|
}
|
|
|
|
var array = value.AsGodotArray();
|
|
var containsNodes = false;
|
|
|
|
for ( var i = 0; i < array.Count; i++ )
|
|
{
|
|
var element = array[ i ];
|
|
|
|
if ( element.VariantType != Variant.Type.Object || element.AsGodotObject() is not Node elementNode )
|
|
{
|
|
continue;
|
|
}
|
|
|
|
array[ i ] = Remap( elementNode, map );
|
|
containsNodes = true;
|
|
}
|
|
|
|
if ( containsNodes )
|
|
{
|
|
duplicate.Set( name, array );
|
|
}
|
|
}
|
|
|
|
static Node Remap( Node originalTarget, Dictionary<Node, Node> map )
|
|
{
|
|
if ( originalTarget != null && map.TryGetValue( originalTarget, out var mapped ) )
|
|
{
|
|
return mapped;
|
|
}
|
|
|
|
return originalTarget;
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif
|