Peculiar bug(?) with deserializing nested user classes from user dll when loading from AssetBundles

Hey all,

I’m having a very peculiar issue with deserialization of MonoBehaviours/ScriptableObjects loaded from asset bundles.

I’m modding a game (i.e. I don’t have the game sources) by loading asset bundles which are compiled from a separate Unity project. The objects are referencing scripts which are compiled separately in Visual Studio into a dll, which is referenced by both the Unity project and the game. Generally all works fine, unless there is a script that has a serializable field of a Type defined in the same dll.

For simplicity, let’s say I have a “Scripts.dll” with the following 2 classes in it:

[CreateAssetMenu(menuName = "ScriptableObjectTest")]
public class ScriptableObjectTest : ScriptableObject, ISerializationCallbackReceiver
{
    public string Name;

    public int Value;

    public Vector3 Vector;

    public TestInternalClass InternalClass = new TestInternalClass();

    public void OnBeforeSerialize()
    {
        MonoBehaviour.print("OnBeforeSerialize: ScriptableObjectTest");
    }

    public void OnAfterDeserialize()
    {
        MonoBehaviour.print("OnAfterDeserialize: ScriptableObjectTest");
    }
}

[Serializable]
public class TestInternalClass : ISerializationCallbackReceiver
{
    public string Name;

    public int Value;

    public Vector3 Vector;

    public void OnBeforeSerialize()
    {
        MonoBehaviour.print("OnBeforeSerialize: TestInternalClass");
    }

    public void OnAfterDeserialize()
    {
        MonoBehaviour.print("OnAfterDeserialize: TestInternalClass");
    }
}

In the Unity project I add a ScriptableObjectTest asset and fill in some random data in the inspector.

Then, for simplicity, let’s say I have a startup scene in the game which has a single object with the following script:

public class TestScript : MonoBehaviour
{
    private void Awake()
    {
        AssetBundle assetBundle = AssetBundle.LoadFromFile(@"path\to\the\asset\bundle");
        ScriptableObjectTest test = assetBundle.LoadAsset<ScriptableObjectTest>("ScriptableObjectTest");
        print(test.ToString());
    }
}

If I run such scene from the editor, or build the player and run the executable, I have the correct output in the log (all of the fields have the values I have assigned in the inspector). This even works if I create a new Unity project and load the asset from it.
But whenever I run it from the modded game, only the parent class and its immediate fields of the primitive or embedded Unity’s Types are deserialized correctly, while the InternalClass always has its default value. For testing purposes I have added the ISerializationCallbackReceiver interfaces to both classes, and whenever the asset is loaded from this game, the TestInternalClass never even gets a deserializer pass (i.e. it’s OnAfterDeserialize() method is never called).

I have tried to match the exact version of Unity with the game (it runs on v5.5.4p1), various AssetBundle build options, nothing seems to fix it, and I’m out of ideas of what may be the cause of this.

I feel like that may be connected to the way unity optimizes stuff when building. If you look here, Bunny83 states that:

If that’s true, I’d assume that’s the very same reason why ISerializationCallbackReceiver doesn’t fire on your custom class - because it hasn’t been somehow registered in the system during a build. Anyway, it’s just my guess.

Is ScriptableObjectTest an original (just modded) class or a one that was added by you?

I have a lot of MonoBehaviours in my dll, and they all work fine unless there are nested classes :slight_smile:
This is a very old answer, I think Unity has advanced the handling of external dlls a lot since 2012 :wink:
ScriptableObjectTest is a new class that derives from ScriptableObject, the code is just above?

Get it. Just wasn’t sure whether you used different dll, or just a recompiled Assembly-CSharp-FirstPass with a modded content.

Since you mention that you have lots of custom MonoBehaviours then yes, the case from the linked question doesn’t apply here.

As for:

I wouldn’t assume that a lot has changed (but I don’t really know)

I’ll try to replicate the case and toy around with it and will come back to you with the results (unless someone provides an answer)

Most likely everything will work for you if you recreate it from scratch, as I’ve already tested it — the asset is deserialized without issues if I try doing it from a project I have created myself (does not matter if this is the same project the asset bundle was compiled from, or a completely separate one).
The bug only happens in the specific game I’m modding, and I’m out of ideas of what may be causing this since the setups are as close as possible — the referenced dll is the same, Unity’s version is the same, the asset bundle is the same. It just doesn’t make any sense, this should work, unless I’m missing something.

If I understand correctly, the point of replicating this is to build an executable without referencing “Scripts.dll”, and then modding it to actually load it. In that case you cannot have:

AssetBundle assetBundle = AssetBundle.LoadFromFile(@"path\to\the\asset\bundle");
        ScriptableObjectTest test = assetBundle.LoadAsset<ScriptableObjectTest>("ScriptableObjectTest");
        print(test.ToString());

in the project right? Because that would mean you are actually referencing the “new stuff” at the build time, which is not how the original game was built. Or did I miss something?

No, the dll is referenced by the game. I have full control over the scripting part, it’s the serialization that’s acting funny. And there are no errors in the logs, the nested classes just don’t get deserialized.

Did you try decompiling the game’s Assembly-CSharp.dll? I’m wondering if they do reference the mod dll the way you presented here or if they load it through reflection? If that’s not a secret, I’d love to know what game are we talking about and try it out by myself :slight_smile:

Oh, but you just game me an idea:
I have built a test game without referencing my “Scripts.dll” and then edited its Assembly-CSharp.dll to reference my custom dll and add the load code from above, and that indeed has reproduced the issue :frowning:
Now I wonder, is it a bug, or just how things work? One could potentially have a plugin-based game which just loads custom dlls at runtime, and apparently their serialization will be broken…

So, apparently the globalgamemanagers.assets file contains some type references that are probably used to guide the serialization… :confused: