godotengine / godot

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

Custom Objects created from C# have the wrong Script resource attached. #48828

Open cgbeutler opened 3 years ago

cgbeutler commented 3 years ago

Godot version: v3.3.1-stable_mono_win64

OS/device including version: Windows 10 64-bit

Issue description: Any Object declared in C#, then instantiated with new MyObject() has an invalid script attached. ResourceLoader uses a resource's resource_path as a kind of "cache key" to avoid reloading things. Each Script resource that is saved to "res://" is usually cached by the ResourceLoader using the script's file path as its resource_path. This ensures that the script is only loaded once. The "Minimal reproduction project" below is fairly small and reveals all in the following snippet:

    public override void _Ready()
    {
        var custom = new CustomNode();
        AddChild(custom);
        var custom_node_script = custom.GetScript() as Script;
        GD.Print( "custom node script path was \"" + custom_node_script.ResourcePath + "\"" );

        var custom2 = GD.Load<CSharpScript>("res://CustomNode.cs").New() as CustomNode;
        AddChild(custom2);
        var custom2_node_script = custom2.GetScript() as Script;
        GD.Print( "Should have been \"" + custom2_node_script.ResourcePath + "\"" );
    }

The above will result in:

custom node script path was ""
Should have been "res://CustomNode.cs"

In other words, when any object is created using the form new CustomNode(), the script that gets attached is not the one cached by ResourceLoader. Instead it is some detached script object. This has major implications when saving and loading. For example, were I to set both nodes' owners to the main scene node and then pack and save it to 'user://Main.tscn', we get the following tscn:

[gd_scene load_steps=4 format=2]

[ext_resource path="res://CustomNode.cs" type="Script" id=1]
[ext_resource path="res://Main.cs" type="Script" id=2]

[sub_resource type="CSharpScript" id=1]

[node name="Main" type="Node2D"]
script = ExtResource( 2 )

[node name="@@2" type="Node" parent="."]
script = SubResource( 1 )

[node name="@@3" type="Node" parent="."]
script = ExtResource( 1 )

The issue can be seen by comparing the first sub_resource to the first ext_resource. The desired save result is the ext_resource, which has a path that can be used to load the script and then the object. The first sub_resource fails as a "Sub Resource" as it should have below it all the information needed to re-create the object. Indeed, if we try to re-load this scene, we get an empty and invalid CSharpScript object that contains no real info. Hopefully that clearly highlights the issue with not having the cached version of the script attached.

Steps to reproduce:

  1. Run the main scene in the minimal project below

OR

  1. Create a custom script in C# that inherits from an Object, like a Node2D or Resource.
  2. Create a second script in C# and construct a new object of the type created in step 1.
  3. Run the second script. The created object will not have the proper script attached.

Minimal reproduction project: https://github.com/cgbeutler/godot_bug_csharp_missing_script_resource_path

neikeq commented 3 years ago

We handle things in a different way for scripts instances from C# rather than from the engine side. I'll have to think how to solve this.

cgbeutler commented 3 years ago

@neikeq, yeah, seems like a tricky one. Also note that not having the version cached by the rest of the engine has other implications, too, since that script resource reference can have metadata and other stuff slapped on it at run-time.

cgbeutler commented 3 years ago

Oh, possible duplicate of #38191

neikeq commented 3 years ago

I think this is happening because we're not setting a path for the CSharpScript we create here:

https://github.com/godotengine/godot/blob/729461b2a455bd3b4afb26bb362863a68e98a9ee/modules/mono/csharp_script.cpp#L2949-L2980

Reason we're not setting is likely because back then we didn't have a way to get the path for a class, but now we do:

https://github.com/godotengine/godot/blob/729461b2a455bd3b4afb26bb362863a68e98a9ee/modules/mono/csharp_script.h#L386 https://github.com/godotengine/godot/blob/729461b2a455bd3b4afb26bb362863a68e98a9ee/modules/mono/csharp_script.h#L69-L72

Not sure if having two CSharpScript instances alive with the same path may cause an issue with Godot's resource system though or if it's fine.

cgbeutler commented 3 years ago

@neikeq Yeah, I realize that this issue is probably really to get the TODO here done: https://github.com/godotengine/godot/blob/729461b2a455bd3b4afb26bb362863a68e98a9ee/modules/mono/csharp_script.cpp#L2954-L2955 The CSharpScript object is not free of state. Scripts are Godot Objects and can hold runtime data like with SetMeta and AddUserSignal. Because of that, using a cached version is the "correct" solution to ensure that all objects with that script share and hot-reload that state.

cgbeutler commented 3 years ago

I've been meaning to look into the pull request here: https://github.com/godotengine/godot/pull/44879 that adds a script server to see if that can be used to complete the TODO. Feel free to beat me to it.

cgbeutler commented 2 years ago

@neikeq Looking at this again, I realize it wasn't clear in my comment before, but I was responding to your remark here

Not sure if having two CSharpScript instances alive with the same path may cause an issue with Godot's resource system though or if it's fine.

When I said "The CSharpScript object is not free of state. Scripts are Godot Objects and can hold runtime data like with SetMeta and AddUserSignal." If you did have two resources with the same path, they would also have unpredictable behavior with registering to the changed signal and stuff. All that said, it could maybe work as a temporary fix, if needed, as state stuff like the above on a script would be extremely rare.

I would also make a note that having the new script take over the path would just shift the problem. Issues like #38191 would suddenly only happen if the script loaded twice, making for a nasty bug that would trigger randomly and blow away saved resource data willy nillily.

cgbeutler commented 2 years ago

@neikeq After digging into the code a bit, it looks like ResourceFormatLoaderCSharpScript::load currently builds a new object every time and sets the path on it:

RES ResourceFormatLoaderCSharpScript::load(const String &p_path, const String &p_original_path, Error *r_error) {
...
    script->set_path(p_original_path);
...

As such, I think your temporary fix of setting script path would work fine. Loading scripts from ResourceLoader has worked fine for me. Eventually both places should have the cache TODO worked out, but this temp fix could get rid of a lot of issues. Pretty sure it would fix https://github.com/godotengine/godot/issues/38191 as well. Have you tried the fix yet?

raulsntos commented 1 year ago

Can't reproduce in Godot 4.0 RC 3, but can still reproduce in Godot 3.5.1 so moving to the 3.x milestone.