Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Godot 4 .Net - Loading / Assigning a script exported from another project fails with error ("Cannot instance script because the associated class could not be found") #75352

Open
Heremeus opened this issue Mar 26, 2023 · 21 comments

Comments

@Heremeus
Copy link

Godot version

v4.0.1.stable.mono.official [cacf499]

System information

Windows 10

Issue description

Exporting a script from one godot project and loading it in another does not work (following https://docs.godotengine.org/en/stable/tutorials/export/exporting_pcks.html). The ResourceLoader.Load<Script>() call finds the script and succeeds, but when assigning the script to a node, the following error occurs:

E 0:00:00:0477   Godot.NativeInterop.NativeFuncs.generated.cs:332 @ void Godot.NativeInterop.NativeFuncs.godotsharp_method_bind_ptrcall(IntPtr , IntPtr , System.Void** , System.Void* ): Cannot instance script because the associated class could not be found. Script: 'res://HelloWorld.cs'. Make sure the script exists and contains a class definition with a name that matches the filename of the script exactly (it's case-sensitive).
  <C++ Error>    Method/function failed. Returning: false
  <C++ Source>   modules/mono/csharp_script.cpp:2301 @ can_instantiate()
  <Stack Trace>  Godot.NativeInterop.NativeFuncs.generated.cs:332 @ void Godot.NativeInterop.NativeFuncs.godotsharp_method_bind_ptrcall(IntPtr , IntPtr , System.Void** , System.Void* )
                 NativeCalls.cs:5208 @ void Godot.NativeCalls.godot_icall_1_583(IntPtr , IntPtr , Godot.Variant )
                 GodotObject.cs:434 @ void Godot.GodotObject.SetScript(Godot.Variant )
                 ImportScript.cs:24 @ void ImportScript._Ready()
                 Node.cs:1783 @ Boolean Godot.Node.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
                 Node3D.cs:988 @ Boolean Godot.Node3D.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
                 ImportScript_ScriptMethods.generated.cs:24 @ Boolean ImportScript.InvokeGodotClassMethod(Godot.NativeInterop.godot_string_name& , Godot.NativeInterop.NativeVariantPtrArgs , Godot.NativeInterop.godot_variant& )
                 CSharpInstanceBridge.cs:24 @ Godot.NativeInterop.godot_bool Godot.Bridge.CSharpInstanceBridge.Call(IntPtr , Godot.NativeInterop.godot_string_name* , Godot.NativeInterop.godot_variant** , Int32 , Godot.NativeInterop.godot_variant_call_error* , Godot.NativeInterop.godot_variant* )

Loading other resources (e.g. scenes) works. The assembly was also loaded successfully. Might be related to this reported issue: .NET 6 - Running exported PCK fails to load scripts or fails to load hostfxr #72159

Steps to reproduce

Note: With the minimal reproduction project, only steps 3-5 and step 8 are required.

  1. Create two new Godot Projects (an exporting and an importing project)
  2. In the exporting, create a new script "HelloWorld" in res:// that prints "Hello World" on _Ready
  3. Export the exporting project (with "Runnable" unchecked) into a .pck file for your platform
  4. Copy the exported .pck file and the project's .dll from .godot\mono\temp\bin\ExportRelease\win-x64
  5. Paste the two files to the res:// folder of the importing project
  6. In the importing project, create a 3D scene and add a new script to the root node
  7. In the new script, paste the _Ready function below
  8. Run the importing project and check the Debugger->Errors list
public override void _Ready()
{
	// Prepare empty node
	Node3D node = new Node3D();
	AddChild(node);

	// Load assembly and pck file
	System.Reflection.Assembly.LoadFile(ProjectSettings.GlobalizePath("res://ExportingProject.dll"));
	ProjectSettings.LoadResourcePack("res://ExportingProject.pck");

	// Load script and assign it
	Script script = ResourceLoader.Load<Script>("res://HelloWorld.cs");
	node.SetScript(script);
}

Minimal reproduction project

MinimalReproductionProjects.zip

@diybl
Copy link

diybl commented Apr 12, 2023

same error

@zkWildfire
Copy link

It would be appreciated if we could get some communication regarding this bug. This affects an engine feature that has its own dedicated page in the Godot docs, but the feature is rendered entirely inoperable for C# projects by this bug. I realize the shift to Godot 4 is a large one and that there are probably a mountain of bugs that need to be dealt with, but it would really nice if we could get even a speculative ETA for this fix. Would it be reasonable to assume that this will be fixed relatively soon, e.g. v4.1 or earlier? Or are there enough higher priority items that we should just assume that this will be broken until v4.2+?

@diybl
Copy link

diybl commented Apr 21, 2023

agree with u。

hotfix is import part for a gameengine.

@RedworkDE
Copy link
Member

It would be appreciated if we could get some communication regarding this bug.

While the docs pages says this works like that and the page claims to be up to date, it is also wrong.

This feature has simply never been implemented after the move to .NET 6 for Godot 4.

@zkWildfire
Copy link

Will fixing this bug be prioritized for Godot 4.2? I see nothing in the Godot 4.1 dev snapshot release notes that would lead me to believe that this is fixed in Godot 4.1.

The fact that there's been no communication regarding when a fix can be expected other than "This feature never worked in Godot 4" is not exactly encouraging. I can't say I'm particularly enthusiastic about the idea of using Godot for game projects if this is the level of support that can be expected for fundamentally broken engine features, let alone smaller bugs.

@AThousandShips
Copy link
Member

AThousandShips commented Jun 2, 2023

This isn't fixed as it's not closed, please check the status of the issue

The fact that there's been no communication regarding when a fix can be expected other than "This feature never worked in Godot 4" is not exactly encouraging. I can't say I'm particularly enthusiastic about the idea of using Godot for game projects if this is the level of support that can be expected for fundamentally broken engine features, let alone smaller bugs.

This is a community driven project and things get worked on as and when people are able, if no one is working on this then nothing will happen, if you are able to work on it yourself you are welcome to! If you think this is the way it works in general then you haven't paid attention, work is going on constantly, just today a bunch of bugs and improvements were resolved, as was the case yesterday, and so on

@zkWildfire
Copy link

Quite honestly, this is one of the most disappointing answers that you could have given. Yes, I am aware that the bug is labeled as still open. Yes, I am aware that hoping that there was progress on the bug was a long shot. Yes, I am aware that every project is going to have bugs and broken features. No, I am not going to invest the significant amount of time it would take to learn the engine systems and implement the feature myself.

Godot as a whole is positioning itself to be a viable alternative to engines such as Unity and especially Unreal Engine when it comes to developing 2D games or 3D games that don't require AAA visuals. However, wanting to have broader appeal means that you can't hide behind the "We're open source, so fix it yourself!" non-answer if you want to be taken seriously. You may consider that answer acceptable from the perspective of an open source project contributor, but it is significantly less so from an outsider's perspective.

Would it be possible for me to learn enough about the engine and implement the feature myself? Perhaps. But that's certainly not always going to be the case, especially since Godot is a beginner friendly engine by nature of having strong 2D support. It's not unreasonable to think that some beginner developer might implement a game in Godot and then decide to learn how to make it moddable and encounter this issue. I would not expect someone with minimal programming experience to dive into Godot's code and implement such a feature themselves.

As it stands, I have very little invested in Godot at the moment. I would like to invest more time into Godot. But answers such as this are not encouraging. Releasing DLC or making a game moddable is not some obscure use case for game developers. If such a feature is so fundamentally broken that it flat out doesn't work, how many other features should I expect to come across that flat out don't work either? I am not willing to invest the considerable amount of time it would take to release a well polished game if the platform I am working with is simply going to tell me to implement or fix it myself any time I come across a bug or broken feature.

@AThousandShips
Copy link
Member

AThousandShips commented Jun 9, 2023

I'm sorry you feel they way, but you're not gonna get any promises that bugs will be fixed anywhere, not here, not with Unity, or any other commercial project, sure they have more resources, but no one can, or should, promise that bugs will be fixed. There's plenty of bugs that have been present in bit commercial projects for years.

It's of course not optimal, but this is a community driven project, based on best efforts and voluntary work. We who contribute here, save for a few people who actually are paid in various ways or sponsored, get no direct reward. And some of us spend significant portions of our spare time doing it, because we want to, and demand nothing in return. And I'm not complaining or demanding praise or thanks, just frankly stating what many of us who make this engine possible do and feel.

And honestly it's disheartening when people complain over having an entirely free engine, with no royalty, no restrictions, and a massive community that pretty much every single day make improvements on some part of it, and it isn't perfect.

I wasn't telling you "if you want it fixed then fix it yourself", I said "if you can help with this you are welcome to!"... And responded to your attitude that implied that bugs reported in this engine are not worked on, responded to, and resolved.

@Zireael07
Copy link
Contributor

Godot is a community ran project and bugs are fixed on best effort basis. If there is no one skilled enough in the area to fix, there is no fix... sadly.

Unfortunately from what I understand C# is one of the areas where it's currently lacking (the two major c# guys decided to go on holidays or just take a break). (The other area where there's a dearth of contributors is physics)

@m1ntkat
Copy link

m1ntkat commented Sep 11, 2023

Is this getting fixed in 4.2

@orolyn
Copy link

orolyn commented Oct 17, 2023

Try adding this to your external project (adjust versions as necessary):

  <ItemGroup>
    <PackageReference Include="Godot.SourceGenerators" Version="4.1.1"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
    <PackageReference Include="GodotSharp" Version="4.1.1" />
  </ItemGroup>
    
  <PropertyGroup>
    <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir>
  </PropertyGroup>

@Heremeus
Copy link
Author

Heremeus commented Oct 18, 2023

Try adding this to your external project (adjust versions as necessary):

  <ItemGroup>
    <PackageReference Include="Godot.SourceGenerators" Version="4.1.1"
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />
    <PackageReference Include="GodotSharp" Version="4.1.1" />
  </ItemGroup>
    
  <PropertyGroup>
    <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir>
  </PropertyGroup>

@orolyn I assume you mean adding those entries to the .csproj file of the exporting project? I tried updating the ExportingProject.csproj in the minimal reproduction project but the error stays the same. Could you provide an example project, if you were able to work around this issue?
For reference, this is the edited ExportingProject.csproj I tried in Godot 4.1.2:

<Project Sdk="Godot.NET.Sdk/4.1.2">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <EnableDynamicLoading>true</EnableDynamicLoading>
    <GodotProjectDir>$(MSBuildProjectDirectory)</GodotProjectDir>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Godot.SourceGenerators" Version="4.1.2" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
    <PackageReference Include="GodotSharp" Version="4.1.2" />
  </ItemGroup>
</Project>

@granitrocky
Copy link

granitrocky commented Nov 12, 2023

OrdinaryGizmos@afa8858

I found a fix. The issue is that for Godot to know that there are scripts in your DLL, it has to call Godot.Bridge.ScriptManagerBridge.LookupScriptsInAssembly(assembly);

Further complicating the matter, the AssemblyAttribute that it looks for is AssemblyHasScriptsAttribute and this is loaded from GodotSharp.dll in a different AssemblyLoadContext than the game.

My patch adds a custom resolver to the Main.cs file in the mono glue that loads GodotSharp.dll (or any dependencies) into the correct AssemblyLoadContext.

After loading the assembly you will need to call Godot.Bridge.ScriptManagerBridge.LookupScriptsInAssembly(assembly);

This was tested on Godot 4.2-dev6 for my game. I plan on opening a pull request.

UPDATE: This fix in the commit is only for tools enabled builds. I am working on a fix for exported releases.

NEW UPDATE: I upgraded to 4.2-beta5 and rewrote my changes. You shouldn't need to modify the engine code at all. All you need to do when loading a DLL is the following 3 lines. In this case dllFile is a byte[] that I preloaded.

        var context = AssemblyLoadContext.GetLoadContext(typeof(Godot.Bridge.ScriptManagerBridge).Assembly);
        var assembly = context.LoadFromStream(new MemoryStream(dllFile));
        Godot.Bridge.ScriptManagerBridge.LookupScriptsInAssembly(assembly);

@Heremeus Can you try out these three lines and see if that fixes it for you?

Heremeus added a commit to Heremeus/godot-docs that referenced this issue Nov 16, 2023
Updating C# assembly loading instructions according to godotengine/godot#75352
@Heremeus
Copy link
Author

@granitrocky Nice find! I can confirm that it worked with your code snippet in godot 4.1.2 as well as in 4.2-beta5. The error is gone and the minimum reproducable project correctly prints "Hello World!".

I opened a pull request to add this info to the docs for now: godotengine/godot-docs#8486

PS: Inspecting a node with an imported script spams error messages in the output but that doesn't seem to cause any problems with the functionality of the script.

Cannot open file 'res://HelloWorld.cs'.
Failed to read file: 'res://HelloWorld.cs'.
Cannot load C# script file 'res://HelloWorld.cs'.
Failed loading resource: res://HelloWorld.cs. Make sure resources have been imported by opening the project in the editor at least once.

@granitrocky
Copy link

@Heremeus

Nice! Glad it's working across releases. That bug about the spam messages is weird. I am seeing that in 4.2-beta5, too. I wonder if it's an ALC thing again or something else...

@iwangxiaodong
Copy link

@Heremeus @granitrocky This way is not available in godot 4.2.1-mono. 😟

@Heremeus
Copy link
Author

@Heremeus @granitrocky This way is not available in godot 4.2.1-mono. 😟

Haven't tried it in 4.2.1 yet but as @raulsntos mentioned in godotengine/godot-docs#8486, the ScriptManagerBridge wasn't meant to be used for this. It's quite possible that they changed things internally and the unintended workaround is gone. But again, I haven't tested it, so can't confirm.

@granitrocky
Copy link

granitrocky commented Jan 25, 2024

@iwangxiaodong Yeah I don't think we should rely on this. It would be better to find a different way to get the dlls dynamically loaded.

However, I do see that LookupScriptsInAssembly is still public in the 4.2 branch, so I think it should work.

This should be all you need to do:

        var context = AssemblyLoadContext.GetLoadContext(typeof(Godot.Bridge.ScriptManagerBridge).Assembly);
        var assembly = context.LoadFromStream(new MemoryStream(dllFile));
        Godot.Bridge.ScriptManagerBridge.LookupScriptsInAssembly(assembly);

@iwangxiaodong
Copy link

iwangxiaodong commented Jan 26, 2024

@granitrocky @Heremeus

Simplified:

AppDomain.CurrentDomain.AssemblyResolve += (s, e) => Assembly.Load(e.Name);
var asm = Assembly.LoadFile(ProjectSettings.GlobalizePath("res://ExportingProject.dll"));
var script = ((GodotObject)Activator.CreateInstance(asm.GetType("HelloWorld"))).GetScript();

@granitrocky
Copy link

@iwangxiaodong

I'm a bit confused. Are you saying that snippet works? I don't think you're supposed to do Assembly.Load inside the AssemblyResolve event. Take a look at The example Here.

@iwangxiaodong
Copy link

@granitrocky

I understand, the key point is LookupScriptsInAssembly:

var alc = AssemblyLoadContext.GetLoadContext(Assembly.GetExecutingAssembly());
var bytes = Godot.FileAccess.GetFileAsBytes("res://ExportingProject.dll");
var asm = alc.LoadFromStream(new MemoryStream(bytes));
ScriptManagerBridge.LookupScriptsInAssembly(asm);
var ok = ProjectSettings.LoadResourcePack("res://ExportingProject.pck");//GD.Print(ok);
var script = GD.Load<Script>("res://HelloWorld.cs");

Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.