#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 nonCopyableProperties = new HashSet() { "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 GetSubtree( Node root ) { var nodes = new List(); 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( 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( 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 MapByPath( Node originalRoot, List originalNodes, Node duplicateRoot ) { var map = new Dictionary(); 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 originalNodes, Node duplicateRoot, Dictionary map ) { var added = new List(); var unresolved = new List(); // 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 originalNodes, Dictionary 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 originalNodes, Dictionary 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 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 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 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 map ) { if ( originalTarget != null && map.TryGetValue( originalTarget, out var mapped ) ) { return mapped; } return originalTarget; } } } #endif