godotengine / godot

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

Validation error using Resource sub-classes and sub-resources with specific combination of preload/@onready/instantiation #95909

Open bkane opened 3 weeks ago

bkane commented 3 weeks ago

Tested versions

System information

Godot v4.3.stable - Windows 10.0.19045 - Vulkan (Mobile) - dedicated NVIDIA GeForce RTX 2080 (NVIDIA; 31.0.15.3713) - Intel(R) Core(TM) i7-9700K CPU @ 3.60GHz (8 Threads)

Issue description

With a very specific set of Resource subclasses and sub-resources, use of preload, use of @onready, and instantiation in script, the following error occurs:

E 0:00:00:0708   validate_object: Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
  <C++ Error>    Condition "!other_script->inherits_script(script)" is true. Returning: false
  <C++ Source>   core/variant/container_type_validate.h:140 @ validate_object()
E 0:00:00:0708   assign: Unable to convert array index 0 from "Object" to "Object".
  <C++ Error>    Method/function failed.
  <C++ Source>   core/variant/array.cpp:238 @ assign()

This occurs when attempting to run the project. The data in the sub-resource will not be loaded, and will be removed from disk when saved. Additionally, validation performed by the editor when loading the project will similarly remove the 'offending' data and give a similar error.

However, it seems that if you don't have a call to instantiate the sub-resource in code (or preload, or @onready in this specific way), then this configuration of resources/subresources is fine to load, use, and save. This may suggest that the class structure is supported but there is some issue arising in the validation process.

Steps to reproduce

Error and automatic deletion of sub-resource at startup

When you open the included minimal reproduction project, you will see the following error in the output window:

Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
Unable to convert array index 0 from "Object" to "Object". 

At this point, the included resource .tres file will be automatically modified to remove the problematic data. You should see its contents go from something like this:

[gd_resource type="Resource" script_class="MyResourceContainer" load_steps=5 format=3 uid="uid://6qhpq8xclwhy"]

[ext_resource type="Script" path="res://my_resource_a.gd" id="1_huj0e"]
[ext_resource type="Script" path="res://my_resource_container.gd" id="2_88hls"]
[ext_resource type="Script" path="res://my_resource_b.gd" id="2_tfgcd"]

[sub_resource type="Resource" id="Resource_aiw63"]
script = ExtResource("2_tfgcd")

[resource]
script = ExtResource("2_88hls")
data = Array[ExtResource("1_huj0e")]([SubResource("Resource_aiw63")])

to this (where the sub_resource has been removed)

[gd_resource type="Resource" script_class="MyResourceContainer" load_steps=3 format=3 uid="uid://6qhpq8xclwhy"]

[ext_resource type="Script" path="res://my_resource_a.gd" id="1_huj0e"]
[ext_resource type="Script" path="res://my_resource_container.gd" id="2_88hls"]

[resource]
script = ExtResource("2_88hls")
data = Array[ExtResource("1_huj0e")]([])

Automatic deletion at runtime if you attempt to add it back

At this point, you could run the project without errors, but that's because the sub-resource has been automatically removed. If you add it back in:

  1. Select test_resource.tres
  2. In the inspector, click Add Element for MyResourceContainer -> Data
  3. Set the new element to New MyResourceB

Now when you run the project, you will see the two errors:

validate_object: Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
assign: Unable to convert array index 0 from "Object" to "Object".

Additionally, the data will not be available if you load the resource. It will also be removed from disk if you save the project.

Interactions with preload, @onready, and more

This is a very specific set of scripts! If you remove the @onready references, the type hints, the function call to instantiate MyResourceB.new(), or the preload call, the issue appears to go away.

I have tried to simplify the project further but this eliminates the issue. For example, simply having "a resource class that contains a sub-resource of a type that itself is a subclass of Resource" is not enough to trigger this error.

Godot 4.2.2:

In v4.2.2.stable.mono.official [15073afe3], the startup error is a little different:

 Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
  unsupported format character
  core/variant/array.cpp:222 - Method/function failed.

The result is the same however, as the sub-resource data is removed from the .tres file.

And at runtime (if you replace the automatically removed sub-resource as described above):

E 0:00:00:0691   validate_object: Attempted to assign an object into a TypedArray, that does not inherit from 'GDScript'.
  <C++ Error>    Condition "!other_script->inherits_script(script)" is true. Returning: false
  <C++ Source>   core/variant/container_type_validate.h:140 @ validate_object()
E 0:00:00:0691   vformat: unsupported format character
  <C++ Error>    Condition "error" is true. Returning: String()
  <C++ Source>   ./core/variant/variant.h:835 @ vformat()
E 0:00:00:0691   assign: Method/function failed.
  <C++ Source>   core/variant/array.cpp:222 @ assign()

Minimal reproduction project (MRP)

resource-bug.zip

matheusmdx commented 3 weeks ago

Bisecting points to #81577 as the culprit:

image

dalexeev commented 3 weeks ago

@anvilfolk ^^

anvilfolk commented 3 weeks ago

@matheusmdx I've noticed you have been doing a lot of bisecting of issues, that's awesome! Like @dalexeev did, it might be worth tagging the author of different PRs so they can follow up!

Circular dependency work, truly the gift that keeps on giving 😅

I will try to take a look at some point though I've got a sick toddler atm so not too soon :) I glanced at the MRP and couldn't immediately find circular references, which is surprising! I'll try to sketch it out and see if I didn't miss something. This might be really hard to track down because of how many different components, so we'll see! :)

beev1s commented 2 weeks ago

I have debugged the MRP. The problem here is not a circular dependency but the order in which resources and scripts are imported paired with the call to GDScriptCache::get_shallow_script that replaced GDScriptCache::get_full_script from https://github.com/godotengine/godot/pull/81577.

The way it works is that when the main_scene.tscn gets loaded, it loads ParentNode.gd which in turn loads ClassThatInstantiates.gd. When ClassThatInstantiates.gd gets compiled ResourceB gets shallow loaded which registers res://my_resource_b.gd in ResourceCache. Afterwards ClassThatPreloads.gd gets loaded which in turns loads test_resource.tres. Here my_resource_b.gd gets loaded, however since it was already registered in the ResourceCache and the loading is done with REUSE it returns the Resource which has not been compiled yet and thus has no _base member set in its script.

Unfortunately, I don't know what the intended behaviour is in this case. @anvilfolk If you can give me a hint, I can try to fix it.

anvilfolk commented 2 weeks ago

Great detective work! 🥰

You're right, the issue isn't cyclic dependencies directly, it's that for them to work we needed to restrict the use of get_full_script to very specific stages of the script compilation pipeline! The original PRs should have some documentation about how/when we can use get_full_script.

I'm afraid I can't offer much in terms of advice right now, I'm stuck with another PR and worked on this too long ago to remember many specifics. Definitely feel free to give it a try though, and join the developer chat for support! There's tests for cyclic dependencies so hopefully if something breaks in the process of figuring out, it'll get caught! If not, we add more tests! ☺️