More @tool woes

Godot Version

4.4

Question

I have a @tool script that, amongst other things, sets a label’s text once one of the exported properties is set:

@tool extends Control

# SU data
@export var su_name : String :
    set(value):
        if value:
            su_name_label.text  = value

Now, this is all good and for each instance of that Control, if I change su_name I also get the label updated accordingly.

The whole thing stops working the next time I open godot.
So as soon as it load the same scene, all labels would be empty and for each of them the following error would appear:

Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.

Once the scene is opened, if I re-edit those, the labels would get correctly updated again.
Is there something in this logic that’s breaking at start up?

Your problem isn’t in the code you posted. It’s likely either in your _ready() function or if you’re loading data your load function. You’re encountering a race condition where you’re trying to set that value before the node is fully constructed.

My ready function is only setting:

func _ready() -> void:
    set_notify_transform(true)

that’s why I didn’t include it.

To be clearer, if I manually drop another new instance of that tscn in there I don’t see any error, everything work as expected. It errors out the next time I open the project.

If su_name_label is an @onready variable it won’t be ready in the editor.
Instead try using the direct get node path:
$my_label_path.text = value

2 Likes

yep, it’s definitely @onready:

@onready var su_name_label:Label        = $su_name_label

and I can confirm that switching in line to:

$su_name_label.text  = value

fixes the issue! Thanks. :+1:

Now… is there any way for me to safely define that su_name_label variable once, then using it across my script, without having to change $su_name_label every time I change the hierarchy/naming of my scene?

Using the onready variable should only be a problem while in the editor.
Running the scene the variable will work.
Ostensibly, the only place it will be necessary to $ (shorthand for get_node()) is in the variable setter.

Using the onready variable should only be a problem while in the editor.

yep, but this is an editor tool, so if that variable fails every time I reopen the scene, then it won’t work.

To give you an idea: if I set that variable to ="foo" I see the label getting changed to “foo”.
Then the next time I open that scene in the editor, all labels will be blank.
If I replace this:

@onready var su_name_label:Label        = $su_name_label
...
su_name_label.text  = value

with this:

$su_name_label.text  = value

it solves that problem, but now everywhere in the script where I want to change or read that label, I’ll have to manually input $su_name_label.somethingSomething, which means I’ll have to keep track and rebase every time I change the name or hierarchical position of the Label node in the scene.

Try just changing it to a normal variable.

var su_name_label:Label = $su_name_label
...
su_name_label.text  = value

that was my original test, which returns the following error

 Parse Error: The default value is using "$" which won't return nodes in the scene tree before "_ready()" is called. Use the "@onready" annotation to solve this. (Warning treated as error.)

Sorry, I wrote that at like 3am. This will work because it waits until the node is fully constructed before assigning the value.

var su_name_label:Label

func _ready() -> void:
	ready.connect(_on_ready)

func _on_ready() -> void:
	su_name_label = $su_name_label

...
su_name_label.text  = value

Thanks.

Sorry, but this still gives me the same error.
I’m doing (I think) exactly what you’re suggesting

var su_name_label:Label

@export var su_name : String :
    set(value):
        if value:
            su_name             = value
            su_name_label.text  = value   #"<---this is where it fails"

func _ready() -> void:
    set_notify_transform(true)
    ready.connect(_on_ready)

func _on_ready() -> void:
    su_name_label = $su_name_label

When I reopen the scene (or reboot godot) I can see su_name being set to the value I had previously input, but the label is still blank, and:
ERROR: res://editor/cell.gd:18 - Invalid assignment of property or key 'text' with value of type 'String' on a base object of type 'Nil'.

line 18 is:
su_name_label.text = value

You are and aren’t doing what I’m suggesting. I forgot you are doing this assignment in a setter. The problem you are running into is a race condition. Specifically, you are setting the value of su_name before su_name_label has been fully initialized and created.

Most likely your problem is that since your variable is an exported variable, it’s running immediately because you’re assigning it a value in the editor. Here’s one solution. It’s hacky, but without knowing more about your program, I don’t have a better one.

var su_name_label:Label
var is_ready = false

@export var su_name : String :
    set(value):
        if value:
            su_name = value
            if is_ready:
                su_name_label.text  = value

func _ready() -> void:
    set_notify_transform(true)
    ready.connect(_on_ready)

func _on_ready() -> void:
    is_ready = true
    su_name_label = $su_name_label
    su_name = su_name
1 Like

Yeah, I see what you mean.

What puzzles me is that this only happens when I reopen the scene again, the su_name variable is already set to the value I had assigned previously and therefore I assumed the setter wouldn’t even trigger.

So when dealing with @tool and exposed parameter, I assume there’s no way to defer the evaluation until the node is ready?
Something like @await_export or @export_on_ready

Whenever you reopen a scene with an @tool script, it is evaluated immediately because it is meant to be run in the editor.

You could split that variable into another scene that doesn’t have a tool script, or you have to deal with the fact that it’s differnt than a normal script.

Not to my knowledge. Which is again, why I gave you the code I did. It’s been the solution that has worked for me.

Typically, after implementing that solution, I usually find another way to deal with the problem down the road. Then I end up refactoring the implementation and the problem goes away on its own.

1 Like

What further processing are you doing with that label in the context of @tool?
Once the program is running and the parent scene is ready you can access that label via the variable.
Be sure and store the value in the export variable.

@export var su_name : String :
    set(value):
    su_name = value

#Note that this if statement is of no use. A string (even "") won't be null and cannot be converted to a bool
        if value:
1 Like

A couple of things…
I’m trying to change the name of the instance of that scene to follow the label.
Something like:

@export var su_name : String :
    set(value):
        su_name = value
        if cell_is_ready:
            name = value    # <---- this line here
            su_name_label.text  = value

Which fails with a new error now:
ERROR: scene/main/node.cpp:1396 - Condition "name.is_empty()" is true.

Judging from the code, it seems to come from:

void Node::_propagate_translation_domain_dirty() {
	for (KeyValue<StringName, Node *> &K : data.children) {  //<- - - - HERE
		Node *child = K.value;
		if (child->data.is_translation_domain_inherited) {
			child->data.is_translation_domain_dirty = true;
			child->_propagate_translation_domain_dirty();
		}
	}

	if (is_inside_tree() && data.auto_translate_mode != AUTO_TRANSLATE_MODE_DISABLED) {
		notification(NOTIFICATION_TRANSLATION_CHANGED);
	}
}

:confused:

#Note that this if statement is of no use. A string (even “”) won’t be null and cannot be converted to a bool

yep yep, that was a remnant of when I had assumed my problem had to do with null data.
Thanks!

Yes, tool scripts are definitely finicky. In testing I received multiple errors and then suddenly no errors.
(Remember to use reload saved scene under the scene menu. I don’t know the exact circumstance but sometimes tool scripts need this to begin working as expected.)

The end result is that the following code produces no errors:

@tool
extends Control

@export var su_name:String: 
	set(value): 
		su_name = value 
		#name = value     # this line renames the node this code is in. I commented it out for testing  
		printt(name, value, "ssss", $Label)
		
		$Label.text = value

It easy to reproduce the error you last quoted. If you try to change the name of a scene to an empty set “” then you get that error. So in your case obviously value is empty when you are assigning it to the name property.
Maybe giving the export var a default value will sidestep the error?
Or an if statement:

if value.length > 0:     
   name = value

I can think of no way to get around direct paths for your situation. But tool scripts are a bit of an unknown to me so maybe someone else can chime in with better solution.

Thanks, I think it’s the same I’m seeing…

So in your case obviously value is empty when you are assigning it to the name property.

Well, it’s tricky because when I assign it, everything works.
It’s when I reopen the scene that Godot goes poopoo.

Maybe giving the export var a default value will sidestep the error?

I’ll try that, but I suspect that as soon as I reopen that scene or relaunch the editor, my name will then be replaced by the default value…

Thanks again.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.