How to filter children of Node by class in Tool script?

Godot Version

4.4.1

Question

I am making a tool script and in it I need to be able to get nodes that have a custom GD script on it. The custom script is ActionNode and nodes I want to find will extend this.

So normally the code would be this

static func filter(node: Node) -> void:
    for child: Node in node.get_children():
        print( child is ActionNode )

Here is a simplified version of my tool script for testing the problem

@tool
extends Node

@export var refresh: bool:
	set(value):
		refresh = value
		_refresh()


@export var node: Node



func _refresh() -> void:
	if not Engine.is_editor_hint():
		return
	print("action tool refresh")
	
	extract(node)


static func extract(node: Node) -> void:
	for child: Node in node.get_children():
		if child.get_script():
			print( child is ActionNode ) 

I have tried many things to be able to get the child nodes that are ActionNodes but since Tool scripts run in editor its a lot harder to get the type of a node, especially since I need to find subclasses (scripts that extend ActionNode).

Some things I have tried:

child is ActionNode = always false

# All ActionNodes have a var ACTION_ID
# child.get_script().get_path() will return the correct path
"ACTION_ID" in child = always false unless ActionNode and subclasses are @tool
"ACTION_ID" in load(child.get_script().get_path()) = always false
"ACTION_ID" in load(child.get_script().get_path()).new() = error (Nonexistent function 'new' in base 'GDScript'.)


var test_node: Node = Node.new()
test_node.set_script(load(child.get_script().get_path()))

test_node is ActionNode = always false unless ActionNode and subclasses are @tool
"ACTION_ID" in test_node = always false unless ActionNode and subclasses are @tool


# I was also told of a way to get the path to all custom classes 
# this gets the path to ActionNode
var test: Dictionary = ProjectSettings.get_global_class_list().get( ProjectSettings.get_global_class_list().find_custom(func(dict: Dictionary): return dict["class"] == &"ActionNode") )

is_instance_of(test_node, load(test["path"])) = false unless ActionNode (not subclass), or if subclasses are @tool
"ACTION_ID" in load(test["path"]) = false
"ACTION_ID" in load(test["path"]).new() = true


# extra: I found that 
# load(test["path"]) returns type Node
# load(child.get_script().get_path()) returns type GDScript

While writing this I found that this behaves differently based on how nodes are added to the tree. Above I added nodes as a Node (white circle icon) then dragged the scripts onto them to add the script. I then tried adding the nodes from the “Create New Node” menu but selecting the ActionNode subclasses. This results in the script icon for the node being greyed out and not changeable, but also results in

child is ActionNode = true
"ACTION_ID" in child = false unless @tool
"ACTION_ID" in test_node = false unless @tool

Is there a way to support the drag and drop method of adding a script to a node, without making all my classes tools? Or will this only work using the menu to select the node class I want?

child is ActionNode should always return true if the script attached to child has ActionNode in its upstream class chain, regardless of how you attached the script. Can you make a minimal example that demonstrates otherwise?

Here are the scripts

TestTool:

@tool
extends Node
class_name TestTool

@export var refresh: bool:
	set(value):
		refresh = value
		_refresh()


@export var node: Node



func _refresh() -> void:
	if not Engine.is_editor_hint():
		return
	print("action tool refresh")
	
	extract(node)


static func extract(node: Node) -> void:
	for child: Node in node.get_children():
		if child.get_script():
			print( child is ActionNode )

ActionNode:

extends Node
class_name ActionNode

var ACTION_ID: StringName = "SomeID"

ActionSubclass:

extends ActionNode
class_name ActionSubclass

Create a scene with two Nodes. Attach TestTool to one and leave the other with no script. Give the empty Node three children that are also Nodes. On one give it the ActionNode script and the other the ActionSubclass, leave the last empty. On the TestTool node set the “node” var in the inspector to the empty parent node that holds the action nodes. Click the “refresh” variable.

The scene tree should look like this:

Node (TestTool)
Node 
|-Node (ActionNode)
|-Node (ActionSubclass)
|-Node

With this you can also test the two methods of adding nodes to the scene to see the different results I got. Method one: add a Node then drag and drop the script onto it in the tree (the script symbol for that node should be white) Method two: add the node by select the script in the “Create New Node” menu (the script symbol for that node should be dark).

1 Like

As expected:

action tool refresh
true
true

I tested it in 4.5 but I doubt 4.4 would behave differently here.

2 Likes

I have to look more into this. I ran my test setup I gave in anew fresh project and it worked as it did for you but my main project still does not return true when expected.

Embarrassingly I found the problem.

I am going through some major refactoring in my project and so all my subclasses of ActionNode have errors in their scripts. once patched so the script is error free it all works.

This was a user error, not a godot error.