godotengine / godot

Godot Engine – Multi-platform 2D and 3D game engine
https://godotengine.org
MIT License
91.34k stars 21.25k forks source link

Editor plugin hooks return immediatly ignoring `await` #98707

Open ydeltastar opened 3 weeks ago

ydeltastar commented 3 weeks ago

Tested versions

4.4.dev

System information

Windows 11 - Vulkan (Forward+)

Issue description

Coroutines don't work in EditorPlugin hooks like:

@tool
extends EditorPlugin

# The project runs immediately and "finished build check" prints after it
func _build() -> bool:
    print("awaiting _build")
    await get_tree().create_timer(3).timeout
    print("finished _build")
    return false

Other hooks used together with EditorPlugin like EditorExportPlugin._customize_resource also return immediately ignoring await. This makes it impossible to customize resources that depend on threaded features like NavigationMesh baking or the render thread.

Steps to reproduce

Run MRP. Build should not return because _build() in "res://addons/plugin/plugin.gd" returns false. But it runs anyway and "Finished Build" is printed 3 seconds later.

Export MRP. It should print "finished _customize_resource" after exporting. But it exports immediately and the message is printed 3 seconds later.

Minimal reproduction project (MRP)

plugin-await-issue.zip

AThousandShips commented 3 weeks ago

Await is not supported in methods called from the engine like this, see:

Note that await doesn't stop execution, it releases execution back to the caller and then resumes after the signal is fired

ydeltastar commented 3 weeks ago

I suppose the only workaround is to customize resources to a temp folder beforehand and just load them at the hooks.

dalexeev commented 2 weeks ago

Note that even GDScript code is not "await-aware" by default. The following code will print 2, then 1, because calling an asynchronous function without await does not wait for the coroutine to complete.

func async_func():
    await get_tree().process_frame
    print(1)

func _ready():
    async_func()
    print(2)

All core APIs are synchronous, all methods execute immediately[^1], coroutines do not exist for the core. Similarly, when the core calls any GDScript function (virtual methods, filter()/map()/reduce() callables, etc.), the core expects the function to be synchronous.

We should clarify this in the GDScript documentation. But I doubt we can do more for void methods[^2]. In theory we could add a static analyzer warning and/or debug runtime validation (coroutines return an GDScriptFunctionState instance), but using await in a native virtual method may be valid, so the warning would be a false positive.

[^1]: However, the effect of the method can be asynchronous, for example call_deferred() adds a call to the message queue, and the call itself will be made later. Same with Tween. [^2]: For non-void methods, we should probably check the return type instead of an implicit conversion.