Conflicts with tool script when loading nodes

Godot Version

4.5 Stable

Question

Hello,

I am currently creating a tool script that takes a mesh and prepares it for further use.

On a button press it takes a glb mesh and then precesses it (creating, removing and moving files). This works fine. The problem is when i restart the engine it gives naming conflics:

The nodes shown here are just a small protion of the nodes being handles. But only these give naming conflics.

Then most of the nodes are deleted and some higher in the scene hierarchy are renamed.

The expected behaviour would be that the nodes and their properties stay saved when i reload the engine.

Code:

@tool
class_name Limb extends Node3D

@export_category(“Actions”)
@export var generate: bool = false : set = _on_btn_generate
@export var clear: bool = false : set = _on_btn_clear

@export_category(“Data”)
@export var mesh: PackedScene
@export var item_data: ItemData

@export_category(“References”)
@export var root_attachment: Node3D
@export var physics_skeleton: Skeleton3D
@export var animation_skeleton: Skeleton3D
@export var target_anim_player: AnimationPlayer
@export var physics_root_bone: PhysicalBone3D

@export_category(“Bone Settings”)

spring stuff

@export var linear_spring_stiffness: float = 400.0
@export var linear_spring_damping: float = 40.0
@export var max_linear_force: float = 9999.0

@export var angular_spring_stiffness: float = 1000.0
@export var angular_spring_damping: float = 80.0
@export var max_angular_force: float = 9999.0

@onready var attachments_container: Node3D = $AttachmentPoints
var physics_bones: Array[PhysicalBone3D] = 

var physics_simulator: PhysicalBoneSimulator3D
var current_delta: float
var ragdoll_mode: bool = false

const SPEED: float = 50
const DAMPING: float = 0.9

func _on_btn_generate(value: bool) → void:
if value == false:
return

await get_tree().process_frame 
generate_structure()
generate = false # Reset switch

func _on_btn_clear(value: bool) → void:
if value == false:
return

_clear_all_generated_nodes()
clear = false

func generate_structure() → void:
# If not inside tree the wait
if not is_inside_tree():
await ready

if not mesh:
	printerr(self.name, ": No mesh assigned!")
	return

print("Owner. ", get_tree().edited_scene_root)
var old_mesh_1 = get_node_or_null("PhysicsMesh")
free_and_clean_node(old_mesh_1)

var old_mesh_2 = get_node_or_null("TargetMesh")
free_and_clean_node(old_mesh_2)

var old_attachment_points = get_node_or_null("AttachmentPoints")
free_and_clean_node(old_attachment_points)

if not mesh:
	return

attachments_container = Node3D.new()
add_child(attachments_container)
attachments_container.owner = get_tree().edited_scene_root
attachments_container.name = "AttachmentPoints"
 
var instance_1 = mesh.instantiate()
instance_1.name = "PhysicsMesh" # Name setzen für das Aufräumen später
add_child(instance_1)
instance_1.owner = get_tree().edited_scene_root
var instance_2 = mesh.instantiate()
instance_2.name = "TargetMesh"
add_child(instance_2)
instance_2.owner = get_tree().edited_scene_root

if Engine.is_editor_hint():
	var tree_root = get_tree().edited_scene_root
	if tree_root:
		_make_all_children_local(instance_1, tree_root)
		_make_all_children_local(instance_2, tree_root)
		
		instance_1.find_child("AnimationPlayer").queue_free()
		physics_skeleton = instance_1.find_child("Skeleton3D",true, false)
		if physics_skeleton:
			var physical_bone_simulator: PhysicalBoneSimulator3D = PhysicalBoneSimulator3D.new()
			physical_bone_simulator.name = "PhysicalBoneSimulator3D"
			physics_skeleton.add_child(physical_bone_simulator)
			physical_bone_simulator.owner = tree_root
			setup_physical_bone_coll(physics_skeleton, 
									physical_bone_simulator)
			var i2_children = instance_2.find_child("Skeleton3D").find_children("*", "",true)
			for child in i2_children:
				child.queue_free()
		
		# Adding the Variables
		animation_skeleton = instance_2.find_child("Skeleton3D",true, false)
		target_anim_player = instance_2.find_child("AnimationPlayer", true)
		#physics_root_bone = 

func setup_physical_bone_coll(skeleton:Skeleton3D, physical_bone_simulator: PhysicalBoneSimulator3D)->void:
for i in range(skeleton.get_bone_count() -1, -1, -1):
var physics_bone: PhysicalBone3D = PhysicalBone3D.new()
physics_bone.bone_name = skeleton.get_bone_name(i)
physical_bone_simulator.add_child(physics_bone)
physics_bone.owner = get_tree().edited_scene_root
physics_bone.name = skeleton.find_children(“*”, “BoneAttachment3D”, true)[0 - i + skeleton.get_bone_count() -1].name

	skeleton.find_children("*", "CollisionShape3D", true)[0].reparent(physics_bone)
	
for bone_attachemnet in skeleton.find_children("*", "BoneAttachment3D", true):
	bone_attachemnet.get_child(0).queue_free()
	bone_attachemnet.queue_free()

func _make_all_children_local(node: Node, new_owner: Node) → void:
for child in node.get_children():

child.owner = new_owner

_make_all_children_local(child, new_owner)

func _clear_owner_recursive(node: Node) → void:
node.owner = null
for child in node.get_children():
_clear_owner_recursive(child)

func _clear_all_generated_nodes() → void:

var names_to_remove = [“PhysicsMesh”, “TargetMesh”, “AttachmentPoints”]
for n_name in names_to_remove:
var node = get_node_or_null(n_name)
free_and_clean_node(node)

func free_and_clean_node(node: Node) → void:
if not node:
return


_clear_owner_recursive(node)


node.name = "TRASH_" + str(node.get_instance_id())

if node.get_parent():
	node.get_parent().remove_child(node)
	

node.queue_free()

A few thoughts.

  1. That script isn’t too long to put in the forum. If you really want to you can hide it in an accordion like this:
    [details=“Summary”]
    ```gd
    #Code here
    ```
    [/details]
    And get this:
Summary
#Code here
  1. That is not the full script. I know this because you said it is a tool script, but there’s no [Tool] decorator at the top of the class definition.
  2. How come you are using a button to do this instead of making this an import script?
  3. Based on the warnings your are seeing, the conflict is potentially because you have multiple similarly-named nodes in that one file?
  1. Shure i changed it
  2. You are right, it cut of the beginning and i did not notice
  3. I have tried to use a import script but don´t really know how to make that work.
  4. When i genere it the first time it works fine with no naming conflicts. When i then restart the Engine i get the naming conflicts and my scene hierarchy is changed. So i suspect it´s maybe trying to override them for some reason ?

Here’s the documentation on it: Import configuration — Godot Engine (stable) documentation in English

What issues were you having? Here’s the script I use to import KayKit models.

import_kaykit_character_as_skin.gd
@tool
extends EditorScenePostImport
#import_kay_kit_character_as_skin.gd

enum LoopMode { LOOP_NONE, LOOP_LINEAR, LOOP_PINGPONG }
const LOOPMODE = ["LOOP_NONE", "[b][color=green]LOOP_LINEAR[/color][/b]", "[b][color=orange]LOOP_PINGPONG[/color][/b]"]
var scene_name: StringName
var owner_scene


# Import script for rig to set up everything so little editing is needed once the character is
# instantiated.
func _post_import(scene):
	owner_scene = scene
	scene_name = scene.name
	print_rich("\n[b][color=red]Begin Post-import[/color] -> [color=purple]%s[/color] as [color=green]%s[/color][/b]" % [scene_name, scene.get_class()])
	print_rich("[b]Time/Date Stamp: %s[/b]\n" % [Time.get_datetime_string_from_system(false, true)])
	prepare(scene)
	enhance(scene)
	scene.name = scene_name + "Skin"
	return scene


func prepare(node: Node) -> void:
	if node is AnimationPlayer:
		for animation_name in node.get_animation_list():
			if animation_name.contains("Idle") or animation_name.contains("ing"):
				node.get_animation(animation_name).set_loop_mode(LoopMode.LOOP_LINEAR)
			print_rich("Post-import: [b]%s[/b] -> [b]%s[/b]" % [animation_name, LOOPMODE[node.get_animation(animation_name).get_loop_mode()]])
	# DeleteBoneAttachment3D nodes because we are going to make our own.
	if node is BoneAttachment3D:
		for item in node.get_children():
			item.owner = null
			item.reparent(owner_scene, false)
			item.owner = owner_scene
			print_rich("Post-import: [b]Moved [color=green]%s[/color] -> Root: [color=yellow]%s[/color][/b]" % [item.get_class(), item.name])
		print_rich("Post-import: [b]Deleted [color=red]%s[/color] -> Slot: [color=yellow]%s[/color][/b]" % [node.get_class(), node.name])
		node.queue_free()
	# We want to rename all the meshes to have more generic names.
	if node is MeshInstance3D:
		node.name = node.name.trim_prefix(scene_name + "_")
		#print_rich("Post-import: [b]Visibile [color=green]%s[/color] [color=yellow]%s[/color][/b] -> [color=yellow][b]%s[/b][/color]" % [node.get_class(), node.name, get_color_string(node.visible)])
	# Rotating the model to face the other direction so it moves in the correct direction when animated.
	if node is Skeleton3D:
		node.rotate_y(deg_to_rad(-180.0))
		print_rich("Post-import: [b]Rotated [color=green]%s[/color] [color=yellow]-180 degrees[/color][/b] on the [b][color=green]y-axis[/color][/b]" % [node.get_class()])
	# Recursively call this function on any subnodes that exist.
	for subnode in node.get_children():
		prepare(subnode)


func enhance(node: Node) -> void:
	#Add slots for head, hands and chest (which is where the capes go).
	var slots: Dictionary = {
		"HeadSlot" : "head",
		"RightHandSlot": "handslot.r",
		"LeftHandSlot": "handslot.l",
		"BackSlot": "chest"
	}
	if node is Skeleton3D:
		for slot in slots:
			var bone_attachment_3d: BoneAttachment3D = BoneAttachment3D.new()
			node.add_child(bone_attachment_3d)
			bone_attachment_3d.owner = owner_scene
			bone_attachment_3d.name = slot
			bone_attachment_3d.bone_name = slots[slot]
			print(bone_attachment_3d)
			print_rich("Post-import: [b]Added [color=green]%s[/color] -> Slot: [color=yellow]%s[/color][/b]" % [slot, slots[slot]])
	# Recursively call this function on any subnodes that exist.
	for subnode in node.get_children():
		enhance(subnode)


# Return rich text color string for true (green)/red (false).
func get_color_string(value: bool):
	if value:
		return "[color=green]true[/color]"
	else:
		return "[color=red]false[/color]"

What’s weird is that your script is running at all when the editor starts. It shouldn’t because it requires a button press to run.

Ok so i looked at import script docs an relized i want to do it with the normal tool script because i need to create two different versions of the glb mesh and that is i think not possible with import (because it always imports the same edited scene).

Yes it is weird because when i use print statements they don´t print, meaning the code is not running. I think it has something to do with it not saving the nodes properly (maybe?).

In that case perhaps try deleting the Godot project file. That might clear it up.

Did not help sadly

@gertkeno @wchc @tibaverus @normalized any ideas?

here is a short video discribing what is happening if that helps: https://drive.google.com/file/d/1A2AFeDNCD4Bz1ORy8USNfjKqJEvZBNfg/view?usp=sharing

Why are instance’s top nodes still displaying the scene icon? What happens when you click on it or try to enable “editable children” for the node?

1 Like

What?! Ok that fixed it. Now i can restart it and no error. Is that a bug or is there a way to do this in code?

Node::set_editable_instance()

Ok thank you and @dragonforge-dev for the help!

1 Like

Although it’s a bit strange that changing the owner wasn’t sufficient. Looking at the source code, engine’s “make local” doesn’t appear to be calling that function.

Making a minimal reproduction example would be beneficial if you want to further investigate.

1 Like

Thanks @normalized

1 Like

Oh, it’s likely top node’s scene_file_path.

Try to just set it to an empty string. I’m curious if it will eliminate the problem without needing to call set_editable_instance().

2 Likes

Yea that was the fix. Thanks!