godot: Mono: Cannot instance a scene with a C# script dependency from PCK.


Bugsquad note: This issue has been confirmed several times already. No need to confirm it further.


Godot version:

v3.2.stable.mono.official

OS/device including version:

Manjaro Linux with kernel 5.5.7-1

Issue description:

We are unable to fully import a scene from a .pck or .zip file with C# scripts dependencies.

E 0:00:00.617   can_instance: Cannot instance script because the class 'PCKScenePrint' could not be found. Script: 'res://PCKScenePrint.cs'.
  <C++ Error>   Method failed. Returning: __null
  <C++ Source>  modules/mono/csharp_script.cpp:2915 @ can_instance()
  <Stack Trace> :0 @ Int32 Godot.NativeCalls.godot_icall_1_186(IntPtr , IntPtr , System.String )()
                SceneTree.cs:637 @ Godot.Error Godot.SceneTree.ChangeScene(System.String )()
                PCKLoader.cs:13 @ void PCKLoader._Ready()()

Steps to reproduce:

  1. Extract the downloaded MinimalProject.zip.

  2. Open the Project_Import_PCK in Godot.

  3. Run the project and check the Debugger.

Minimal reproduction project:

MinimalProject.zip

Notes:

  • Export directory contains the exported .pck and .zip files.
  • Project_Export_PCK is the PCK’s project directory.
  • Project_Import_PCK is the directory of the project that import’s the .pck or .zip file.

About this issue

  • Original URL
  • State: open
  • Created 4 years ago
  • Reactions: 8
  • Comments: 18 (3 by maintainers)

Most upvoted comments

Well, at least it worked but it’s really unstable and i sure i’m doing it really poorly.

So here’s what i’ve done in the PCKLoader.cs:

private byte[] GetAssemblyBytes()
{
    byte[] rawAssembly = null;

    Godot.File dllFile = new Godot.File();
    dllFile.Open("res://.mono/assemblies/Debug/ProjectExportPCK.dll", Godot.File.ModeFlags.Read);
    rawAssembly = dllFile.GetBuffer((int) dllFile.GetLen());
    dllFile.Close();

    return rawAssembly;
}

private void FixDependencies()
{
    Assembly asm = AppDomain.CurrentDomain.Load(GetAssemblyBytes());

    foreach (string dependencyPath in ResourceLoader.GetDependencies("res://PCKScene.tscn"))
    {
        // Look for C# Scripts
        CSharpScript csScript = ResourceLoader.Load<CSharpScript>(dependencyPath, "CSharpScript", false);

        // Makes sure it's a C# script
        if(csScript == null)
        {
            continue;
        }

        // Try to reload the script
        Error reloadResult = csScript.Reload();

        // Look for each type in the assembly of the DLC
        foreach (Type type in asm.GetTypes())
        {
            // Look if the type name is the same as the resource name
            // Potential error depending on the name
            if(type.ToString().Equals(csScript.ResourceName))
            {
                // Try to instance the script with Activator
                object scriptInstance = Activator.CreateInstance(type);

                // In this case the script derives from Control type
                Control control = (Control) scriptInstance;
                
                // Try to instance the script as a control node
                GetTree().CurrentScene.AddChild(control);
                break;
            }
        }
    }
}

So right now, i rather prefer to not do something like this. It is very unstable, crashes Godot one time out of two, not even the game itself.

The best we can do right now with the stable version is to do all the code in the same assembly and separate all other “heavy” assets in PCKs.

@diybl This issue is about C# in 3.x, which uses Mono, not .NET 6. The .NET 6 issue is being tracked in https://github.com/godotengine/godot/issues/73932.

I believe I have found a stable work around. This example below is setup to only load one specific mod (GameMod) but can be adapted to do more than one mod.

I’ve attached the projects if someone wants to test further. The Game.zip would be your game itself, containing the mod loader and an example mod. And the GameMod.zip would be what someone else makes as the mod to your game.

Hopefully this helps with creating a fix in the engine but it runs as a good work around in the meantime from my tests.

public class Main : Node2D {

	public const string NameSpace = "GameMod"; //Namespace of the mod
	public static Assembly Assembly; //Assembly of the mod
	
	public override void _Ready() {
		base._Ready();
		
		var dir = Directory.GetCurrentDirectory() + "\\";
		foreach (var file in Directory.GetFiles("Mods")) {
			if (!file.ToLower().EndsWith(".dll")) continue;

			Assembly = Assembly.LoadFile(dir + file);
			ProjectSettings.LoadResourcePack((dir + file).Replace(".dll", ".pck"));
		}

		var entryPoint = LoadPatched("res://ModEntry.tscn");
		AddChild(entryPoint.Instance());
	}
	
        //Loads a PackedScene but replaces the CScript with an Assembly instantiated version
	public static PackedScene LoadPatched(string scenePath) {
		var x = ResourceLoader.Load<PackedScene>(scenePath);

		var bundled = x.Get("_bundled") as Dictionary;

		var vars = bundled["variants"] as Array;

		for (var i = 0; i < vars.Count; i++) {
			var o = vars[i];
			if (o is CSharpScript c) {
				//Try to find the path of the script by the namespace and the resource itself
				var path = NameSpace + "." + c.ResourcePath.Split("res://")[1].Replace("/", ".").Replace(".cs", "");
				
				//Attempt to instantiate that script, assuming it is a Node so we can get the script
				var thing = Assembly.GetType(path).GetConstructor(Type.EmptyTypes)?.Invoke(System.Array.Empty<object>()) as Node;
				//Change the script of the packed scene
				vars[i] = thing.GetScript();
				thing.Free();
			}
		}

		bundled["variants"] = vars;

		x.Set("_bundled", bundled);
		return x;
	}

}

GameMod.zip Game.zip

I am also facing this problem.