godotengine / godot

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

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

Open nargacu83 opened 4 years ago

nargacu83 commented 4 years ago

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:

Ch3sta23 commented 4 years ago

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?

nargacu83 commented 4 years ago

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.

spacecomplexity commented 4 years ago

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.

nargacu83 commented 4 years ago

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.

Wavertron commented 4 years ago

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

Calinou commented 4 years ago

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

nargacu83 commented 4 years ago

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.

akien-mga commented 4 years ago

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

Host32 commented 3 years ago

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.

wenbingzhang commented 2 years ago

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

valkyrienyanko commented 2 years ago

I am also facing this problem.

gkazan commented 2 years ago

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

wenbingzhang commented 2 years ago

我相信我已经找到了稳定的工作。下面的示例设置为仅加载一个特定的 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?

diybl commented 1 year ago

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

diybl commented 1 year ago

PCKLoader

godot4 error;;

Assembly.GetType(path) this objet is null

diybl commented 1 year ago

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

Calinou commented 1 year ago

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

diybl commented 1 year ago

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

dangerz commented 1 month ago

Any updates on this? Still having the issue and can't seem to find a workaround.