godot icon indicating copy to clipboard operation
godot copied to clipboard

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

Open nargacu83 opened this issue 4 years ago • 18 comments


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.

nargacu83 avatar Mar 05 '20 18:03 nargacu83

I'm facing this problem too. In your project the Assembly.LoadFile call (mentioned here: https://docs.godotengine.org/en/3.2/getting_started/workflow/export/exporting_pcks.html ) is missing but even with the assembly loaded the script cant be instanced

@xsellier Since you wrote that note in the docs, could you help us out?

Ch3sta23 avatar Mar 26 '20 08:03 Ch3sta23

I didn't had the time to test it but i think we can try to load the DLL's content in the PCK with the godot File class, try to convert it into bytes to load it using Assembly.Load(bytes[] rawAssembly).

Yes we wrote that in the docs when i had this problem, so maybe we can find a workaround for it soon.

nargacu83 avatar Mar 26 '20 08:03 nargacu83

Was this problem ever figured out? I'm getting the following error when trying to load a scene with a c# script attached to it: ERROR: Cannot instance script because the class 'Tutorial' could not be found.

spacecomplexity avatar Aug 22 '20 19:08 spacecomplexity

Not yet, after some research i ended up doing all the code on the base and only do textures, scenes and other assets in the PCKs like in other engines.

I wanted to do more research before posting here. So far i found that the assembly is indeed loaded but Godot don't know it, all the code is there but can't be "loaded" in the Godot context.

nargacu83 avatar Aug 28 '20 22:08 nargacu83

Any progress on this? I'm getting the error just trying to run my project via the Editor or VSCode

Wavertron avatar Sep 11 '20 01:09 Wavertron

@Wavertron As far as I know, nobody found a solution to this yet.

Calinou avatar Sep 11 '20 07:09 Calinou

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.

nargacu83 avatar Sep 12 '20 14:09 nargacu83

@neikeq I guess that's a use case that you hadn't thought of/tested yet. Some changes are probably needed to ensure that assemblies are properly exported in "DLC" type PCKs, and properly loaded when using ProjectSettings.LoadResourcePack.

akien-mga avatar Dec 01 '20 08:12 akien-mga

Same problem here.

I created a new project to test and its attached here: mono-test.zip

Steps to reproduce:

  1. Move KinematicBody2D.cs and KinematicBody2D.tscn to outside the project and export it as executable.
  2. Move KinematicBody2D.cs and KinematicBody2D.tscn again to inside the project and export it as .pck or .zip to extract the .dll
  3. Open the exported game and fill the paths to paths

If i export the executable game filtering the resources, but with the class present on project it works properly, but if a export the executable without the classes and after try to load new classes in runtime the error apeear.

Host32 avatar Feb 04 '21 03:02 Host32

I also found this problem today, and I hope it can be solved.

wenbingzhang avatar Feb 21 '22 13:02 wenbingzhang

I am also facing this problem.

valkyrienyanko avatar Apr 08 '22 04:04 valkyrienyanko

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

gkazan avatar Jul 27 '22 18:07 gkazan

我相信我已经找到了稳定的工作。下面的示例设置为仅加载一个特定的 mod (GameMod),但可以调整为执行多个 mod。

如果有人想进一步测试,我已经附上了这些项目。Game.zip 将是您的游戏本身,包含 mod 加载器和示例 mod。GameMod.zip 将是其他人制作的游戏模组。

希望这有助于在引擎中创建修复程序,但同时从我的测试中可以很好地解决它。

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;
	}

}

游戏模组.zip 游戏 .zip

Does it support IOS?

wenbingzhang avatar Oct 10 '22 01:10 wenbingzhang

肯定不支持啊, 可以看看hybridclr这个。。但是他们目前没打算支持godot

diybl avatar Mar 01 '23 08:03 diybl

PCKLoader

godot4 error;;

Assembly.GetType(path) this objet is null

diybl avatar Mar 01 '23 11:03 diybl

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

godot4 error

Fatal error. System.AccessViolationException: Attempted to read or write protected memory. This is often an indication that other memory is corrupt. at Godot.NativeInterop.NativeFuncs.godotsharp_string_new_with_utf16_chars(Godot.NativeInterop.godot_string ByRef, Char*) at Godot.NativeInterop.Marshaling.ConvertStringToNative(System.String) at Godot.NativeInterop.NativeFuncs.godotsharp_string_name_new_from_string(System.String)

var thing = asm.GetType(path).GetConstructor(Type.EmptyTypes)?.Invoke(System.Array.Empty()) as Node;

this line crashed

diybl avatar Mar 01 '23 16:03 diybl

@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.

Calinou avatar Mar 01 '23 17:03 Calinou

@diybl This issue is about C# in 3.x, which uses Mono, not .NET 6. The .NET 6 issue is being tracked in #73932.

Are there plans to fix this bug? I'm worried my game won't have this feature before release. Or could I use the patch method of c# to temporarily solve this bug?

diybl avatar Mar 05 '23 03:03 diybl