rokojori_action_library/Tools/Editor/NodeDuplicator.cs

195 lines
4.8 KiB
C#
Raw Permalink Normal View History

2026-10-07 08:29:15 +00:00
#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.
public static class NodeDuplicator
{
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 duplicate = original.Duplicate( (int) Node.DuplicateFlags.Default );
parent.AddChild( duplicate );
parent.MoveChild( duplicate, original.GetIndex() + 1 );
SetOwnerRecursively( duplicate, original.Owner );
RemapExternalReferences( original, duplicate );
duplicate.LogInfo( "Duplicated", original.Name, "as", duplicate.Name );
return duplicate;
}
static void SetOwnerRecursively( Node node, Node sceneOwner )
{
if ( sceneOwner == null )
{
return;
}
node.Owner = sceneOwner;
foreach ( Node child in node.GetChildren() )
{
SetOwnerRecursively( child, sceneOwner );
}
}
static void RemapExternalReferences( Node originalRoot, Node duplicateRoot )
{
var originalNodes = new List<Node>();
var duplicateNodes = new List<Node>();
NodesWalker.Get().Iterate( originalRoot, n => originalNodes.Add( n ), false );
NodesWalker.Get().Iterate( duplicateRoot, n => duplicateNodes.Add( n ), false );
var originalToDuplicate = new Dictionary<Node, Node>();
for ( var i = 0; i < originalNodes.Count && i < duplicateNodes.Count; i++ )
{
originalToDuplicate[ originalNodes[ i ] ] = duplicateNodes[ i ];
}
for ( var i = 0; i < originalNodes.Count; i++ )
{
RemapNodeExports( originalNodes[ i ], duplicateNodes[ i ], originalToDuplicate );
}
}
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;
}
var remapped = Remap( targetNode, map );
if ( remapped != targetNode )
{
duplicate.Set( name, remapped );
}
}
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 changed = 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;
}
var remapped = Remap( elementNode, map );
if ( remapped != elementNode )
{
array[ i ] = remapped;
changed = true;
}
}
if ( changed )
{
duplicate.Set( name, array );
}
}
static Node Remap( Node originalTarget, Dictionary<Node, Node> map )
{
if ( map.TryGetValue( originalTarget, out var mapped ) )
{
return mapped;
}
return originalTarget;
}
}
}
#endif