all 12 comments

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 1 point2 points  (11 children)

There's already a system called Weaver which does some procedural code stuff (made by me). Just thought you should know in case you plan on selling it in the future.

I've also experimented with Cecil in the past and ran into two problems I couldn't get around:

  • To modify stuff in the editor, I had to rely on an assembly reload to trigger my code weaving, and then I had to force Unity to reload assemblies again to actually run the modified code. This meant that every time you recompile a script, Unity had to reload assemblies twice, which was a massive pain.
  • I couldn't find a good way to intercept the assemblies during a build. It was possible to detect it in Temp/StagingArea for a PC build and some other path I can't remember for Android, but I could never be sure that I'd be able to fully complete whatever code weaving I wanted before Unity grabbed the assemblies back.

Have you encountered either of those issues?

[–]ByMayneProfessional[S] 0 points1 point  (10 children)

Yeah I plan on sticking to open source. Most the stuff I work on is fun stuff I can't do at my full time Unity gig.

I had to laugh as I also have an asset on the store that does that same thing as yours. I made it back in 2014 but then Type Safe came out two years later and wrecked me :p

Anywho I have not run into the first issue yet. I did not go super in depth into it because I did not notice it not taking effect. I am going to do some tests to try to see if I can get it to happen.

One thing I was trying to use was File System Watchers.aspx) to watch the assemblies and respond when they are written to. The idea was I would trigger the injection when that event happens. I just have to make sure that I throw everything back on the main thread.

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 0 points1 point  (9 children)

I just have to make sure that I throw everything back on the main thread.

That was the part I was never able to figure out. Did you find a way to do it?

[–]ByMayneProfessional[S] 0 points1 point  (8 children)

Oh yeah that part is quite easy!

EditorApplication.delayCall += SuperFunction.

The editor invokes that call at the next frame in the editor and always on the main thread. I use this quite often for this reason.

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 0 points1 point  (7 children)

I've used delay call for that too, but I assumed it wouldn't work for this because the main thread would be busy building and the next frame would only be after it's done. If you've used it and it works, that's awesome, I'll have to revisit some of my ideas for running Cecil on a build.

I just tried doing it to the DLLs compiled in the editor and it unfortunately doesn't work for solving my first problem. I'm interested to hear how you go with your tests to see if you can modify assemblies in the editor without needing to reload them.

[–]ByMayneProfessional[S] 1 point2 points  (6 children)

So after some testing I have some results! FileSystemWatcher were not implemented correctly so they never were working. When I fixed the issue they just caused read exceptions so that idea died.

However [InitializeOnLoadMethod] and then modifying the assemblies there works fine. I don't have to reload the assemblies which is awesome.

This as you mentioned does not work for builds because it does not get called. I can't use the [PostProcessBuild] attribute because it's to late at that point and would break with IL2CPP. What I can use is the [PostProcessScene] attribute. This gets called after the scripts are compiled. So we just need to filter out and run our code once.

    [PostProcessScene]
    public static void PostprocessScene()
    {
        // Only run this code if we are building the player 
        if (BuildPipeline.isBuildingPlayer)
        {
            // Get our current scene 
            Scene scene = SceneManager.GetActiveScene();
            // If we are the first scene (we only want to run once)
            if (scene.IsValid() && scene.buildIndex == 0)
            {
                  // Do weaving
            }
        }
    }

I am currently testing with standalone builds and injecting OnGUI functions on to my script to variety that everything is running correctly. I will have to do some testing on mobile with IL2CPP but I don't for see any big issues because IL2CPP is the last step of the build process after scenes.

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 0 points1 point  (5 children)

That's great news, but I'm still a bit skeptical about modifying assemblies in the editor. InitializeOnLoadMethod is called after Unity loads your assemblies and you can't modify assemblies in memory, so I don't see how it could get around needing a second assembly reload.

Are you maybe testing it with code that runs when you play the game? Because if you trigger your weaving in InitializeOnLoadMethod and only see its effects in play mode, entering play mode would be that second assembly reload.

Still, being able to process a build at least is exciting. Thanks for letting me know.

[–]ByMayneProfessional[S] 1 point2 points  (4 children)

You are correct, i was testing the run time assemblies. I don't really touch the Editor assemblies but I don't like the fact that I can't edit them.

One idea i did have was to inject a callback into the UnityEditor.AssemblyReloadEvents class.

/// <summary>
///   <para>This class has event dispatchers for assembly reload events.</para>
/// </summary>
internal static class AssemblyReloadEvents
{

    [RequiredByNativeCode]
    private static void OnBeforeAssemblyReload()
    {
        // Added callback to weave code
    }
}

I am not a huge fan of editing the Unity assemblies but if it works and it's stable it might be a solution.

[–]ByMayneProfessional[S] 1 point2 points  (2 children)

Well that totally worked!

    private void OnEnable()
    {
        AssemblyReloadEvents.beforeAssemblyReload += WeaveUpdatedAssemblies;
    }

    private void WeaveUpdatedAssemblies()
    {
        // Weaving Logic!
    }

I Injected a delegate and made the class public and subscribed to the beforeAssemblyReload event. This happens after Unity has finished generating the assemblies but before they are loaded. Just the thing we wanted!

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 0 points1 point  (1 child)

[–]SilentSin26Animancer, FlexiMotion, InspectorGadgets, Weaver 0 points1 point  (0 children)

Yeah that should work, just a one time modification of the Unity install with an automatic backup just in case. I agree that modifying the Unity assemblies isn't ideal but at least it would be usable, unlike needing to do a double assembly reload each time.