Autoload in plugin lead to ParseError when adding it in a fresh project

Godot Version

Godot 4.5

Question

Hi! I am making my first plugin. The plugin works correctly after a project reload when the autoload is enabled at least once, but fails with parse and class resolution errors on initial activation in a fresh project.

Here is how my addon works:

Overview

The plugin follows this initialization flow:

  1. Plugin Activation (_enable_plugin): Registers an autoload via add_autoload_singleton() and defers UI setup
  2. Autoload Initialization (_ready): Instantiates an ARKitServer and connects signals
  3. Menu UI: The editor panel communicates with the autoload via signals

Initial Errors

When first adding the plugin to a new project, two parse errors occur:

res://addons/godotarkit/arkit_server.gd:163 - Parse Error: Identifier "ARKitSingleton" not declared in the current scope.
res://addons/godotarkit/arkit_menu.gd:91 - Parse Error: Identifier "ARKitSingleton" not declared in the current scope.

After enabling the plugin, a runtime error appears:

res://addons/godotarkit/arkit_autoload.gd:52 - Invalid call. Nonexistent function 'new' in base 'GDScript'.
Error at: '_server = ARKitServer.new(11111)'

The class ARKitServer mysteriously resolves to the base GDScript type instead of the actual class.

Code Structure

Plugin Core (plugin.gd):

@tool
extends EditorPlugin
const ARKIT_AUTOLOAD_NAME: String = "ARKitSingleton"
const ARKIT_MENU: String = "uid://bmmmwgdvt6ymb"
const ArkitAutoload: String = "arkit_autoload.gd"

var control: Control

func _enable_plugin() -> void:
	add_autoload_singleton(ARKIT_AUTOLOAD_NAME, ArkitAutoload)
	call_deferred("_setup_ui")

func _setup_ui() -> void:
	control = load(ARKIT_MENU).instantiate()
	add_control_to_bottom_panel(control, "GodotARKit")

func _disable_plugin() -> void:
	if control:
		remove_control_from_bottom_panel(control)
		control.queue_free()
	remove_autoload_singleton(ARKIT_AUTOLOAD_NAME)

Autoload (arkit_autoload.gd):

@tool
extends Node

var _subjects: Dictionary[String, ARKitSubject] = {}
var _server  # <- Missing type annotation

func _ready() -> void:
	_server = ARKitServer.new(11111)
	change_port.connect(_on_change_port)
	start_server.connect(_server.start)
	stop_server.connect(_server.stop)

func _exit_tree() -> void:
	if is_instance_valid(_server):
		_server.stop()

Menu UI (arkit_menu.gd):

@tool
extends HBoxContainer

func _ready() -> void:
	_subjects_list.clear()
	_hide_subject()
	ARKitSingleton.show_error.connect(_on_error_shown)
	ARKitSingleton.add_subject.connect(_on_add_subject)
	ARKitSingleton.remove_subject.connect(_on_remove_subject)

Note on Classes: ARKitServer, ARKitSubject, and related classes are standard RefCounted classes without the @tool annotation.

ARKitServer is not a tool, it’s just a RefCounted, same for all other classes

Real question

I don’t understand why I have those errors only when first loading, and how to resolve them so that when a user download my addon, it does not create any error

  • It can be that I don’t understand the behavior of @tool and that it cause some weird things
  • It can be that I don’t fully understand the parser order or something related
  • I also encountered a weird Internal script error! Opcode: something (please report) by enabling/disabling the plugin in a new project, but it only happened once.

So I’m at a loss here, since it works… after a project reload. I don’t want to publish an addon that causes errors for the user, and I have no clue where the problem is.

Thanks for reading this!
Could you please help me :frowning:

Code that’s not @tool won’t run in the editor.

Yeah but where do you see code that’s not @tool being run in the editor?

I may not understand it, but for me the problem occurs before any script is run, it’s a first parsing problem. ARKitSingleton cannot be declared beforehand since it’s an autoload, but has not been created either since the script is being imported/parsed for the first time.

Every other class is RefCounted, I don’t see why it shouldn’t work, every child of ‘Node’ in this addon is also a @tool

Are those RefCounted @tool? If there is some code in those classes that is called by other tool scripts, that code won’t run unless it’s also @tool

I added @tool to all RefCounted as you suggested. All the errors remain.

From what I understand, the @tool exists so that the related scripts can access the editor scene tree. A RefCounted is never in the scene tree, it should act like a basic variant type, thus why I don’t see the need of adding this keyword (I could be wrong).

Plus, if it didn’t work, the plugin should never work, but it does, after a reload when the autoload is registered for the first time.

When I create a fresh project, create a singleton manually, then add my addons folder, there are 0 initial errors (obviously I shadowed my addon autoload name).

No, @tool has nothing to do with the scene tree. Read up the link I posted above.

Check that you don’t have any circular references.
Also check that you don’t have any constructors that take arguments, without default values for those arguments provided.

I would really like you to provide explanations to your claim, if I don’t understand something please let me know why. From what I read:

@tool scripts run inside the editor, and let you access the scene tree of the currently edited scene. This is a powerful feature which also comes with caveats, as the editor does not include protections for potential misuse of @tool scripts. Be extremely cautious when manipulating the scene tree, especially via Node.queue_free, as it can cause crashes if you free a node while the editor runs logic involving it.

Furthermore, I did a new plugin, a MINIMAL one, that represent my problem better. Here is the full code:

EditorPlugin code

@tool
extends EditorPlugin
const AUTOLOAD_NAME: String = "AutoloadTest"
const AUTOLOAD_PATH:String = "res://addons/minimal_bug/autoload_test.gd"


func _enable_plugin() -> void:
	add_autoload_singleton(AUTOLOAD_NAME, AUTOLOAD_PATH)


func _disable_plugin() -> void:
	remove_autoload_singleton(AUTOLOAD_NAME)

Autoload code:

@tool
extends Node

static var thing:int

Thingy.gd, that is never instanced nor referenced, just a script here, floating inside the addons folder, abandonned:

@tool
extends Node
func _never_called():
	AutoloadTest.thing

The problem is the same as I referenced, ParseError, so the user has to enable the plugin, then reload the editor for the errors to disappear.
The errors:

  ERROR: res://addons/minimal_bug/thingy.gd:5 - Parse Error: Identifier "AutoloadTest" not declared in the current scope.
  ERROR: modules/gdscript/gdscript.cpp:3041 - Failed to load script "res://addons/minimal_bug/thingy.gd" with error "Parse error".

Well if autoload is not present, the script fails to parse, so the engine reports that. If you want to use the autoload this way, don’t reference it by name. Inject the reference from _enable_plugin() into scripts/nodes that need it.

Yeah, chicken and egg problem without forward declaration:

Inject the reference from _enable_plugin() into scripts/nodes that need it.

What do you mean?

The second solution may not be horrible, but makes you lose autocomplete. It’s a shame that we cannot do forward declarations, using a singleton to do thing on the behalf of the user and giving him toolkits on the form of Objects is not a strange idea.

In my case, i can put the signals in the server, it makes more sense.

Possible Autoloads should be interpreted as valid classes by the parser, it would solve this problem.

You can put things into static properties/methods instead of autoload, unless you explicitly need that autoloaded node’s functionality.

I think that just asking the user to reload the editor after enabling the addon may be a more pragmatic solution. I would not be the first.
We’ll wait for the plugin/autoloads things to be reworked.

Imo autoloads should be avoided whenever possible. And in most cases, static classes are a better alternative. Using autoloads as namespaces for persistent data is a bad habit inherited from 3.x days where there wasn’t any better options.