RealityStop / Bolt.Addons.Community

A community-driven project for extending Unity Bolt
MIT License
250 stars 34 forks source link

AOT-stubs for Bolt Community Addons in Addressables on Android #67

Closed MarkusFynd closed 3 months ago

MarkusFynd commented 4 months ago

Hello! We are heavily relying on using addressables in our project. To support logic in our addressable asset packs, we've started looking into Visual Scripting, and it seems this is not supported out of the box on AOT-platforms such as Android, since all logic has to be pre-compiled in the base project. We've been able to have some success by following the steps described in this thread to generate AOT-stubs, however we are still having some issues with the community addons package.

The error is as follows: ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.Community.OnUnityEvent::OneParamHandler<System.Single>' for which no ahead of time (AOT) code was generated. As per the above linked thread, we've modified the AotPreBuilder script to include the needed namespaces in order to generate AOT-stubs for the needed APIs. I've tried to include the following namespaces:

We're still getting the above exception in Android builds, although it seems to work on standalone Windows builds. We've also tried with and without engine code stripping enabled. We have also tried to include a reference to the Bolt.Addons.Community.Runtime asmdef, and made a dummy script with a direct reference to a OnUnityEvent node, to no avail.

OnUnityEvent is the node we have mainly used in our testing to simplify our graphs (although we'd like to use more of the addon nodes). Does this node specifically, or the rest of the package, have other dependencies we need to include in the AotPreBuilder script? Are there other steps we can take in trying to figure this out? Any and all insight or tips on this matter is greatly appreciated. Thanks!

MarkusFynd commented 4 months ago

@S2NX7 might you have some insight on this?

MarkusFynd commented 4 months ago

Just wanted to add: Clicking "Compile" in the Window > Community Addons > Utilities window only generates empty folders inside the Unity.VisualScripting.Community.Generated folder, no C# scripts. Also, we are using v1.9.1 of the UVS package and v3.1.2 (v4.0.4 according to the Unity package manager) of the community addons package.

S2NX7 commented 4 months ago

Just wanted to add: Clicking "Compile" in the Window > Community Addons > Utilities window only generates empty folders inside the Unity.VisualScripting.Community.Generated folder, no C# scripts. Also, we are using v1.9.1 of the UVS package and v3.1.2 (v4.0.4 according to the Unity package manager) of the community addons package.

Hey so this is for the Code assets to Generate The C# for them so if you don't have any code assets it wont generate any script.

Now with your Issue I could give you code that you can try I am not sure if it will work though but you can try it you just need to replace the OnUnityEvent script with this.


using System;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{

    internal class OnUnityEventData : EventUnit<EventData>.Data
    {
        public object EventListener { get; set; }
    }

    public class EventData
    {
        public object Value0 { get; set; }
        public object Value1 { get; set; }
        public object Value2 { get; set; }
        public object Value3 { get; set; }
    }

    [UnitTitle("On Unity Event")]
    [UnitCategory("Events")]
    public class OnUnityEvent : EventUnit<EventData>, IAotStubbable
    {
        protected override bool register => false;

        [DoNotSerialize]
        public ValueInput UnityEvent;

        public Type Type { get; private set; }

        public override IGraphElementData CreateData()
        {
            return new OnUnityEventData();
        }

        protected override void Definition()
        {
            base.Definition();

            UnityEvent = ValueInput<UnityEventBase>("event");

            if (Type != null)
            {
                var genericArguments = Type.GetGenericArguments();
                for (var i = 0; i < genericArguments.Length; i++)
                {
                    ValueOutput(genericArguments[i], $"arg{i}");
                }
            }
        }

        public override void StartListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener != null || !UnityEvent.hasValidConnection) return;

            UpdatePorts();

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
            var delegateType = method?.GetParameters()[0].ParameterType;

            data.EventListener = CreateAction(delegateType, stackRef);

            method?.Invoke(eventBase, new[] { data.EventListener });
        }

        public override void StopListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener == null) return;

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.RemoveListener));
            method?.Invoke(eventBase, new[] { data.EventListener });

            data.EventListener = null;
        }

        public void UpdatePorts()
        {
            Type = GetEventType();
            Define();
        }

        private Type GetEventType()
        {
            var eventType = UnityEvent?.connection?.source?.type;

            while (eventType != null && eventType.BaseType != typeof(UnityEventBase))
            {
                eventType = eventType.BaseType;
            }

            return eventType;
        }

        private object CreateAction(Type delegateType, GraphReference reference)
        {
            var numParams = delegateType.GetGenericArguments().Length;

            if (numParams == 0)
            {
                void Action()
                {
                    Trigger(reference, new EventData());
                }

                return (UnityAction)Action;
            }

            string methodName;

            if (numParams == 1) methodName = nameof(OneParamHandler);
            else if (numParams == 2) methodName = nameof(TwoParamsHandler);
            else if (numParams == 3) methodName = nameof(ThreeParamsHandler);
            else methodName = nameof(FourParamsHandler);

            var method = GetType().GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.InvokeMethod);

            return method?.MakeGenericMethod(delegateType.GetGenericArguments()).Invoke(this, new object[] { reference });
        }

        internal UnityAction<T> OneParamHandler<T>(GraphReference reference)
        {
            return arg0 =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0
                });
            };
        }

        internal UnityAction<T0, T1> TwoParamsHandler<T0, T1>(GraphReference reference)
        {
            return (arg0, arg1) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1
                });
            };
        }

        internal UnityAction<T0, T1, T2> ThreeParamsHandler<T0, T1, T2>(GraphReference reference)
        {
            return (arg0, arg1, arg2) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2
                });
            };
        }

        internal UnityAction<T0, T1, T2, T3> FourParamsHandler<T0, T1, T2, T3>(GraphReference reference)
        {
            return (arg0, arg1, arg2, arg3) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2,
                    Value3 = arg3
                });
            };
        }

        protected override void AssignArguments(Flow flow, EventData args)
        {
            var numOutputs = valueOutputs.Count;

            if (numOutputs > 0) flow.SetValue(valueOutputs[0], args.Value0);
            if (numOutputs > 1) flow.SetValue(valueOutputs[1], args.Value1);
            if (numOutputs > 2) flow.SetValue(valueOutputs[2], args.Value2);
            if (numOutputs > 3) flow.SetValue(valueOutputs[3], args.Value3);
        }

        private OnUnityEventData GetData(GraphPointer stack)
        {
            return stack.GetElementData<OnUnityEventData>(this);
        }

        public override IEnumerable<object> GetAotStubs(HashSet<object> visited)
        {
            yield return typeof(OnUnityEvent).GetMethod(nameof(OneParamHandler), BindingFlags.Instance | BindingFlags.NonPublic);
            yield return typeof(OnUnityEvent).GetMethod(nameof(TwoParamsHandler), BindingFlags.Instance | BindingFlags.NonPublic);
            yield return typeof(OnUnityEvent).GetMethod(nameof(ThreeParamsHandler), BindingFlags.Instance | BindingFlags.NonPublic);
            yield return typeof(OnUnityEvent).GetMethod(nameof(FourParamsHandler), BindingFlags.Instance | BindingFlags.NonPublic);
        }
    }
}
MarkusFynd commented 4 months ago

Now with your Issue I could give you code that you can try I am not sure if it will work though but you can try it you just need to replace the OnUnityEvent script with this.

Thanks for the updated code! I moved the community addons package from Library\PackageCache directly into the Packages folder, where the manifest.json file is located, and removed the community addons from manifest.json. Then I updated the OnUnityEvent script with the code you provided. I did this in both the base project, as well as the asset project where we build our addressables.

Unfortunately, this didn't work. In fact, it introduced some new exceptions, and the script graphs we had working on Android has now stopped working. PS - my IDE also warned me that Base interface 'IAotStubbable' is redundant because OnUnityEvent inherits 'EventUnit<EventData>'. It seems like all Unit types use IGraphElement, which already uses IAotStubbable. Here are the new errors:

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.StaticPropertyAccessor`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]::.ctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Exception& exc) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.DoInvoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.Invoke (System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder 

NullReferenceException: Object reference not set to an instance of an object.
  at Unity.VisualScripting.GraphPointer.GetElementData[T] (Unity.VisualScripting.IGraphElementWithData element) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.EventUnit`1[TArgs].StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.XGraphEventListener.StopListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.ScriptMachine.OnDisable () [0x00000] in <00000000000000000000000000000000>:0 

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.InstancePropertyAccessor`2[[UnityEngine.Transform, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null],[UnityEngine.Vector3, UnityEngine.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null]]::.ctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Exception& exc) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.DoInvoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at Syst

NullReferenceException: Object reference not set to an instance of an object.
  at Unity.VisualScripting.GraphPointer.GetElementData[T] (Unity.VisualScripting.IGraphElementWithData element) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.EventUnit`1[TArgs].StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.XGraphEventListener.StopListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.ScriptMachine.OnDisable () [0x00000] in <00000000000000000000000000000000>:0 

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.StaticPropertyAccessor`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]::.ctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Exception& exc) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.DoInvoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeConstructorInfo.Invoke (System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder 

NullReferenceException: Object reference not set to an instance of an object.
  at Unity.VisualScripting.GraphPointer.GetElementData[T] (Unity.VisualScripting.IGraphElementWithData element) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.EventUnit`1[TArgs].StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StopListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.XGraphEventListener.StopListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.ScriptMachine.OnDisable () [0x00000] in <00000000000000000000000000000000>:0 

It seems like all these new errors are similar to the initial one, saying no AOT code was generated, then followed by a null ref exception. So there might be a way to get Unity to generate that AOT code, but in any case it's the same exception as the initial one, so if I could have solved that one, I bet this one would get fixed too.

To confirm, I reverted back the changes, and the graphs that stopped working started working again, and the original exception returned:

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.Community.OnUnityEvent::OneParamHandler<System.Single>' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeMethodInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Exception& exc) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.RuntimeMethodInfo.Invoke (System.Object obj, System.Reflection.BindingFlags invokeAttr, System.Reflection.Binder binder, System.Object[] parameters, System.Globalization.CultureInfo culture) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Reflection.MethodBase.Invoke (System.Object obj, System.Object[] parameters) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.StartListening (U

Any other input on this issue is appreciated.

MarkusFynd commented 4 months ago

Another interesting observation. If I use the OnUnityEvent node with a Button OnClick event, that doesn't have a parameter, it works! If used with a component that has a parameter, such as a Slider OnValueChanged event, it doesn't work.

S2NX7 commented 4 months ago

Another interesting observation. If I use the OnUnityEvent node with a Button OnClick event, that doesn't have a parameter, it works! If used with a component that has a parameter, such as a Slider OnValueChanged event, it doesn't work.

Ok so then the problem should be that the methods used are generic methods I am a bit busy right now so I will have to come up with a fix later I hope that is ok.

S2NX7 commented 4 months ago

Ok so I have a Idea you will have to change 2 scripts the OnUnityEvent and the AOT Support script it makes the classes Generic instead of the methods Here is the updated scripts:

AOT Support

using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{
    public class AotUnityEvent<T1> : UnityEvent<T1>
    {
        internal UnityAction<T1> Use(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return OneParamHandler(reference, onUnityEvent);
        }

        private UnityAction<T1> OneParamHandler(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return arg0 =>
            {
                onUnityEvent.Trigger(reference, new EventData
                {
                    Value0 = arg0
                });
            };
        }
    }

    public class AotUnityEvent<T1, T2> : UnityEvent<T1, T2>
    {
        internal UnityAction<T1, T2> Use(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return TwoParamsHandler(reference, onUnityEvent);
        }

        private UnityAction<T1, T2> TwoParamsHandler(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return (arg0, arg1) =>
            {
                onUnityEvent.Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1
                });
            };
        }
    }

    public class AotUnityEvent<T1, T2, T3> : UnityEvent<T1, T2, T3>
    {
        internal UnityAction<T1, T2, T3> Use(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return ThreeParamsHandler(reference, onUnityEvent);
        }

        private UnityAction<T1, T2, T3> ThreeParamsHandler(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return (arg0, arg1, arg2) =>
            {
                onUnityEvent.Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2
                });
            };
        }
    }

    public class AotUnityEvent<T1, T2, T3, T4> : UnityEvent<T1, T2, T3, T4>
    {
        internal UnityAction<T1, T2, T3, T4> Use(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return FourParamsHandler(reference, onUnityEvent);
        }

        private UnityAction<T1, T2, T3, T4> FourParamsHandler(GraphReference reference, OnUnityEvent onUnityEvent)
        {
            return (arg0, arg1, arg2, arg3) =>
            {
                onUnityEvent.Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2,
                    Value3 = arg3
                });
            };
        }
    }
}

OnUnityEvent

using System;
using System.Reflection;
using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{

    internal class OnUnityEventData : EventUnit<EventData>.Data
    {
        public object EventListener { get; set; }
    }

    public class EventData
    {
        public object Value0 { get; set; }
        public object Value1 { get; set; }
        public object Value2 { get; set; }
        public object Value3 { get; set; }
    }

    [UnitTitle("On Unity Event")]
    [UnitCategory("Events")]
    public class OnUnityEvent : EventUnit<EventData>
    {
        protected override bool register => false;

        [DoNotSerialize]
        public ValueInput UnityEvent;

        public Type Type { get; private set; }

        public override IGraphElementData CreateData()
        {
            return new OnUnityEventData();
        }

        protected override void Definition()
        {
            base.Definition();

            UnityEvent = ValueInput<UnityEventBase>("event");

            if (Type != null)
            {
                var genericArguments = Type.GetGenericArguments();
                for (var i = 0; i < genericArguments.Length; i++)
                {
                    ValueOutput(genericArguments[i], $"arg{i}");
                }
            }
        }

        public override void StartListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener != null || !UnityEvent.hasValidConnection) return;

            UpdatePorts();

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
            var delegateType = method?.GetParameters()[0].ParameterType;

            data.EventListener = CreateAction(delegateType, stackRef);

            method?.Invoke(eventBase, new[] { data.EventListener });
        }

        public override void StopListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener == null) return;

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.RemoveListener));
            method?.Invoke(eventBase, new[] { data.EventListener });

            data.EventListener = null;
        }

        public void UpdatePorts()
        {
            Type = GetEventType();
            Define();
        }

        private Type GetEventType()
        {
            var eventType = UnityEvent?.connection?.source?.type;

            while (eventType != null && eventType.BaseType != typeof(UnityEventBase))
            {
                eventType = eventType.BaseType;
            }

            return eventType;
        }

        private object CreateAction(Type delegateType, GraphReference reference)
        {
            var numParams = delegateType.GetGenericArguments().Length;

            if (numParams == 0)
            {
                void Action()
                {
                    Trigger(reference, new EventData());
                }
                return (UnityAction)Action;
            }
            else if (numParams == 1)
            {
                var aotUnityEventType = typeof(AotUnityEvent<>).MakeGenericType(delegateType.GetGenericArguments());
                var useMethod = aotUnityEventType.GetMethod("Use", BindingFlags.Instance | BindingFlags.NonPublic);
                var aotUnityEventInstance = Activator.CreateInstance(aotUnityEventType);
                return useMethod?.Invoke(aotUnityEventInstance, new object[] { reference, this });
            }
            else if (numParams == 2)
            {
                var aotUnityEventType = typeof(AotUnityEvent<,>).MakeGenericType(delegateType.GetGenericArguments());
                var useMethod = aotUnityEventType.GetMethod("Use", BindingFlags.Instance | BindingFlags.NonPublic);
                var aotUnityEventInstance = Activator.CreateInstance(aotUnityEventType);
                return useMethod?.Invoke(aotUnityEventInstance, new object[] { reference, this });
            }
            else if (numParams == 3)
            {
                var aotUnityEventType = typeof(AotUnityEvent<,,>).MakeGenericType(delegateType.GetGenericArguments());
                var useMethod = aotUnityEventType.GetMethod("Use", BindingFlags.Instance | BindingFlags.NonPublic);
                var aotUnityEventInstance = Activator.CreateInstance(aotUnityEventType);
                return useMethod?.Invoke(aotUnityEventInstance, new object[] { reference, this });
            }
            else
            {
                var aotUnityEventType = typeof(AotUnityEvent<,,,>).MakeGenericType(delegateType.GetGenericArguments());
                var useMethod = aotUnityEventType.GetMethod("Use", BindingFlags.Instance | BindingFlags.NonPublic);
                var aotUnityEventInstance = Activator.CreateInstance(aotUnityEventType);
                return useMethod?.Invoke(aotUnityEventInstance, new object[] { reference, this });
            }
        }

        protected override void AssignArguments(Flow flow, EventData args)
        {
            var numOutputs = valueOutputs.Count;

            if (numOutputs > 0) flow.SetValue(valueOutputs[0], args.Value0);
            if (numOutputs > 1) flow.SetValue(valueOutputs[1], args.Value1);
            if (numOutputs > 2) flow.SetValue(valueOutputs[2], args.Value2);
            if (numOutputs > 3) flow.SetValue(valueOutputs[3], args.Value3);
        }

        private OnUnityEventData GetData(GraphPointer stack)
        {
            return stack.GetElementData<OnUnityEventData>(this);
        }

    }
}
MarkusFynd commented 4 months ago

Sorry about the delayed response, and thanks again for the updated code! Unfortunately, this didn't work either. I get a similar error, but for a different method this time:

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.Community.AotUnityEvent`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]::.ctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 

Once again this is only the case for the OnUnityEvent node with a parameter, it works without parameters.

MarkusFynd commented 4 months ago

Hmm, this actually broke the working graphs too I think. The above comment was made after replacing the scripts you provided in the base project, but when I replaced them in the addressable build project as well, my testing script graph that uses the OnUnityEvent without parameters doesn't seem to work either. The error is the same as in the above comment:

ExecutionEngineException: Attempting to call method 'Unity.VisualScripting.Community.AotUnityEvent`1[[System.Single, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]::.ctor' for which no ahead of time (AOT) code was generated.
  at System.Reflection.RuntimeConstructorInfo.InternalInvoke (System.Object obj, System.Object[] parameters, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at System.Activator.CreateInstance (System.Type type, System.Boolean nonPublic, System.Boolean wrapExceptions) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StartListening (Unity.VisualS
S2NX7 commented 4 months ago

Ok so the only other solution I can come up with is making a way to generate a script that will have the Generics already Typed instead of trying to do it dynamically to attempt add support for each event you will have to make sure that you generate the script for the Nodes.

S2NX7 commented 4 months ago

It wont let me send the files directly so you will have to make 2 of them I will send the code.

OnUnityEventsAotSupport(Make sure to name each file exactly as I put it)

using System.Collections.Generic;
using UnityEngine;
using System;
using System.Linq;

namespace Unity.VisualScripting.Community
{
    [CreateAssetMenu(menuName = "Visual Scripting/Community/AotSupport", fileName = "OnUnityEvent Support")]
    public class OnUnityEventsAotSupport : ScriptableObject
    {
        [HideInInspector]
        public List<TypeGroup> typesToSupport = new List<TypeGroup>();

        public class TypeGroup : IEquatable<TypeGroup>
        {
            public TypeGroup(params Type[] types)
            {
                this.types = types.ToList();
            }

            public List<Type> types = new List<Type>();

            public bool Equals(TypeGroup other)
            {
                if (other == null || other.types.Count != types.Count)
                {
                    return false;
                }

                for (int i = 0; i < types.Count; i++)
                {
                    if (types[i] != other.types[i])
                    {
                        return false;
                    }
                }

                return true;
            }

            public override bool Equals(object obj)
            {
                if (obj is TypeGroup other)
                {
                    return Equals(other);
                }
                return false;
            }

            public override int GetHashCode()
            {
                unchecked
                {
                    int hash = 17;
                    foreach (var type in types)
                    {
                        hash = hash * 23 + type.GetHashCode();
                    }
                    return hash;
                }
            }
        }
    }
}

GenerateAotSupport(Make sure this is in a folder named "Editor")

 using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";
            Debug.Log("Generated OnUnityEvent Support script at : " + path);
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            string scriptContent = @"
using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();

            string[] scriptGraphAssetGuids = AssetDatabase.FindAssets($"t:{typeof(ScriptGraphAsset)}");
            string[] sceneGUIDs = AssetDatabase.FindAssets("t:Scene");
            int totalProgressSteps = scriptGraphAssetGuids.Length + sceneGUIDs.Length;
            int currentProgress = 0;

            foreach (var scriptGraphAssetGuid in scriptGraphAssetGuids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(scriptGraphAssetGuid);
                ScriptGraphAsset scriptGraphAsset = AssetDatabase.LoadAssetAtPath<ScriptGraphAsset>(assetPath);
                var events = scriptGraphAsset.graph.GetUnitsRecursive(Recursion.New(100));
                if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                {
                    foreach (OnUnityEvent item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                    {
                        var type = item.UnityEvent.connection.source.type;
                        var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                        var delegateType = method?.GetParameters()[0].ParameterType;
                        types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                    }
                }
                currentProgress++;
                EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Script Graphs...", (float)currentProgress / totalProgressSteps);
            }

            foreach (string sceneGUID in sceneGUIDs)
            {
                string scenePath = AssetDatabase.GUIDToAssetPath(sceneGUID);
                EditorSceneManager.OpenScene(scenePath);
                var scene = EditorSceneManager.GetSceneByPath(scenePath);

                if (!scene.IsValid()) continue;
                foreach (var gameObject in scene.GetRootGameObjects())
                {
                    if (gameObject.TryGetComponent<ScriptMachine>(out var scriptMachine))
                    {
                        if (scriptMachine != null && scriptMachine.nest.embed != null)
                        {
                            var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                            if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                            {
                                foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                                {
                                    var type = item.UnityEvent.connection.source.type;
                                    var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                    var delegateType = method?.GetParameters()[0].ParameterType;
                                    types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                                }
                            }
                        }
                    }
                    currentProgress++;
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Scenes...", (float)currentProgress / totalProgressSteps);
                }
            }

            return types;
        }
    }
}

OnUnityEvent(You can just update the current script you have you don't need to make a new one)

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{

    internal class OnUnityEventData : EventUnit<EventData>.Data
    {
        public object EventListener { get; set; }
    }

    public class EventData
    {
        public object Value0 { get; set; }
        public object Value1 { get; set; }
        public object Value2 { get; set; }
        public object Value3 { get; set; }
    }

    [UnitTitle("On Unity Event")]
    [UnitCategory("Events")]
    public class OnUnityEvent : EventUnit<EventData>
    {
        protected override bool register => false;

        [DoNotSerialize]
        public ValueInput UnityEvent;

        public Type Type { get; private set; }

        public override IGraphElementData CreateData()
        {
            return new OnUnityEventData();
        }

        protected override void Definition()
        {
            base.Definition();

            UnityEvent = ValueInput<UnityEventBase>("event");

            if (Type != null)
            {
                var genericArguments = Type.GetGenericArguments();
                for (var i = 0; i < genericArguments.Length; i++)
                {
                    ValueOutput(genericArguments[i], $"arg{i}");
                }
            }
        }

        public override void StartListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener != null || !UnityEvent.hasValidConnection) return;

            UpdatePorts();

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
            var delegateType = method?.GetParameters()[0].ParameterType;

            data.EventListener = CreateAction(delegateType, stackRef);

            method?.Invoke(eventBase, new[] { data.EventListener });
        }

        public override void StopListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener == null) return;

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.RemoveListener));
            method?.Invoke(eventBase, new[] { data.EventListener });

            data.EventListener = null;
        }

        public void UpdatePorts()
        {
            Type = GetEventType();
            Define();
        }

        private Type GetEventType()
        {
            var eventType = UnityEvent?.connection?.source?.type;

            while (eventType != null && eventType.BaseType != typeof(UnityEventBase))
            {
                eventType = eventType.BaseType;
            }

            return eventType;
        }

        private object CreateAction(Type delegateType, GraphReference reference)
        {
            var numParams = delegateType.GetGenericArguments().Length;

            if (numParams == 0)
            {
                void Action()
                {
                    Trigger(reference, new EventData());
                }
                return (UnityAction)Action;
            }

            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";

            if (File.Exists(path))
            {
                Type aotSupportMethodsType = GetAotSupportMethodsType();
                var method = aotSupportMethodsType.GetMethods().First(method => method.ReturnType == delegateType);
                return method?.Invoke(null, new object[] { reference, this });
            }
            else
            {
                throw new InvalidOperationException("No AotSupportMethods script Found");
            }

        }

        internal UnityAction<T1> OneParamHandler<T1>(GraphReference reference)
        {
            return arg0 =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0
                });
            };
        }

        internal UnityAction<T1, T2> TwoParamsHandler<T1, T2>(GraphReference reference)
        {
            return (arg0, arg1) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1
                });
            };
        }

        internal UnityAction<T1, T2, T3> ThreeParamsHandler<T1, T2, T3>(GraphReference reference)
        {
            return (arg0, arg1, arg2) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2
                });
            };
        }

        internal UnityAction<T1, T2, T3, T4> FourParamsHandler<T1, T2, T3, T4>(GraphReference reference)
        {
            return (arg0, arg1, arg2, arg3) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2,
                    Value3 = arg3
                });
            };
        }

        protected override void AssignArguments(Flow flow, EventData args)
        {
            var numOutputs = valueOutputs.Count;

            if (numOutputs > 0) flow.SetValue(valueOutputs[0], args.Value0);
            if (numOutputs > 1) flow.SetValue(valueOutputs[1], args.Value1);
            if (numOutputs > 2) flow.SetValue(valueOutputs[2], args.Value2);
            if (numOutputs > 3) flow.SetValue(valueOutputs[3], args.Value3);
        }

        private OnUnityEventData GetData(GraphPointer stack)
        {
            return stack.GetElementData<OnUnityEventData>(this);
        }

        private Type GetAotSupportMethodsType()
        {

            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

            foreach (Assembly assembly in assemblies)
            {
                Type type = assembly.GetType("AotSupportMethods");

                if (type != null)
                {
                    return type;
                }
            }

            return null;

        }
    }
}
S2NX7 commented 4 months ago

Once you make it in the Project tab Right click then click Create >Visual Scripting > Community > Aot Support

and then on the Aot Support Press Generate Aot Support.

That should Hopefully fix your problem.

MarkusFynd commented 4 months ago

Thanks! I'm getting the following error when clicking the "Generate Aot Support" button:

ArgumentException: Cannot open scene with path "Packages/com.unity.render-pipelines.universal/Editor/SceneTemplates/Basic.unity".
UnityEditor.SceneManagement.EditorSceneManager.OpenScene (System.String scenePath, UnityEditor.SceneManagement.OpenSceneMode mode) (at <11d97693183d4a6bb35c29ae7882c66b>:0)
UnityEditor.SceneManagement.EditorSceneManager.OpenScene (System.String scenePath) (at <11d97693183d4a6bb35c29ae7882c66b>:0)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.FindGraphsAndScenes () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:157)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.GenerateScript () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:29)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.OnInspectorGUI () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:21)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass59_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <b0f293012f4f4a13b3c5a7dc37df7ba8>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

Sorry about all the back and forth, I appreciate all your help!

I haven't mentioned it, but we are mostly using script machines on GameObjects and/or prefabs, with "Source" set to "Embed". I don't know if this affects the way you generate AOT support, since you're going over scenes.

S2NX7 commented 4 months ago

I made it go over scenes to get all the embed graphs so it should not have problems with this. For some reason its going through the package scenes ok let me see if I can fix that.

S2NX7 commented 4 months ago

Ok so you can change the GenerateAotSupport script to this

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";
            Debug.Log("Generated OnUnityEvent Support script at : " + path);
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            string scriptContent = @"
using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();

            string[] scriptGraphAssetGuids = AssetDatabase.FindAssets($"t:{typeof(ScriptGraphAsset)}");
            string[] sceneGUIDs = AssetDatabase.FindAssets("t:Scene");
            int totalProgressSteps = scriptGraphAssetGuids.Length + sceneGUIDs.Length;
            int currentProgress = 0;

            foreach (var scriptGraphAssetGuid in scriptGraphAssetGuids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(scriptGraphAssetGuid);
                ScriptGraphAsset scriptGraphAsset = AssetDatabase.LoadAssetAtPath<ScriptGraphAsset>(assetPath);
                var events = scriptGraphAsset.graph.GetUnitsRecursive(Recursion.New(100));
                if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                {
                    foreach (OnUnityEvent item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                    {
                        var type = item.UnityEvent.connection.source.type;
                        var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                        var delegateType = method?.GetParameters()[0].ParameterType;
                        types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                    }
                }
                currentProgress++;
                EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Script Graphs...", (float)currentProgress / totalProgressSteps);
            }

            foreach (string scenePath in FindScenesInAssetsFolder())
            {
                EditorSceneManager.OpenScene(scenePath);
                var scene = EditorSceneManager.GetSceneByPath(scenePath);

                if (!scene.IsValid()) continue;
                foreach (var gameObject in scene.GetRootGameObjects())
                {
                    if (gameObject.TryGetComponent<ScriptMachine>(out var scriptMachine))
                    {
                        if (scriptMachine != null && scriptMachine.nest.embed != null)
                        {
                            var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                            if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                            {
                                foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                                {
                                    var type = item.UnityEvent.connection.source.type;
                                    var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                    var delegateType = method?.GetParameters()[0].ParameterType;
                                    types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                                }
                            }
                        }
                    }
                    currentProgress++;
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Scenes...", (float)currentProgress / totalProgressSteps);
                }
            }

            return types;
        }

        private List<string> FindScenesInAssetsFolder()
        {
            List<string> scenePaths = new List<string>();

            string[] sceneGUIDs = AssetDatabase.FindAssets("t:Scene");

            foreach (string sceneGUID in sceneGUIDs)
            {
                string scenePath = AssetDatabase.GUIDToAssetPath(sceneGUID);

                // Check if the scene is within the Assets folder
                if (scenePath.StartsWith("Assets/"))
                {
                    scenePaths.Add(scenePath);
                }
            }

            return scenePaths;
        }
    }
}
S2NX7 commented 4 months ago

Sorry about all the back and forth, I appreciate all your help!

Its no problem at all, You're welcome!

Just tell me if it works or not.

MarkusFynd commented 4 months ago

One step closer. The graph with the OnUnityEvent with no parameters is working again. However I now have this error when running:

InvalidOperationException: No AotSupportMethods script Found
  at Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.XGraphEventListener.StartListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.ScriptMachine.OnEnable () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.Object.Instantiate[T] (T original) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.ResourceManagement.ResourceProvide

And the OnUnityEvent node with a parameter doesn't work.

I also noticed that the generated script is just empty:


using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
}
S2NX7 commented 4 months ago

Wow that is weird it's generating correctly for me. I am a bit busy right now so give me like a hour or so then I will try and figure this out.

MarkusFynd commented 4 months ago

On Windows (editor), I get this exception which I guess makes sense since the generated script is emtpy:

InvalidOperationException: Sequence contains no matching element
System.Linq.Enumerable.First[TSource] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] predicate) (at <1c318258bf0843289b0e2cbe692fee39>:0)
Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) (at Packages/dev.bolt.addons@f8d1c5e2b1/Runtime/Events/Nodes/OnUnityEvent.cs:124)
Unity.VisualScripting.Community.OnUnityEvent.StartListening (Unity.VisualScripting.GraphStack stack) (at Packages/dev.bolt.addons@f8d1c5e2b1/Runtime/Events/Nodes/OnUnityEvent.cs:69)
Unity.VisualScripting.FlowGraph.StartListening (Unity.VisualScripting.GraphStack stack) (at Packages/com.unity.visualscripting@1.9.2/Runtime/VisualScripting.Flow/FlowGraph.cs:48)
Unity.VisualScripting.XGraphEventListener.StartListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) (at Packages/com.unity.visualscripting@1.9.2/Runtime/VisualScripting.Core/Listeners/IGraphEventListener.cs:18)
Unity.VisualScripting.ScriptMachine.OnEnable () (at Packages/com.unity.visualscripting@1.9.2/Runtime/VisualScripting.Flow/ScriptMachine.cs:22)
UnityEngine.ResourceManagement.AsyncOperations.<>c__DisplayClass57_0:<add_CompletedTypeless>b__0(AsyncOperationHandle`1)
DelegateList`1:Invoke(AsyncOperationHandle`1) (at Library/PackageCache/com.unity.addressables@1.19.19/Runtime/ResourceManager/Util/DelegateList.cs:69)
UnityEngine.AsyncOperation:InvokeCompletionEvent()
S2NX7 commented 4 months ago

Ok maybe you can try this :

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs" + "\nEnsure that the script stays here";
            Debug.Log("Generated OnUnityEvent Support script at : " + path);
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            string scriptContent = @"using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();

            string[] scriptGraphAssetGuids = AssetDatabase.FindAssets($"t:{typeof(ScriptGraphAsset)}");
            var scenePaths = EditorBuildSettings.scenes.Select(s => s.path).ToArray();
            int totalProgressSteps = scriptGraphAssetGuids.Length + scenePaths.Length;
            int currentProgress = 0;

            foreach (var scriptGraphAssetGuid in scriptGraphAssetGuids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(scriptGraphAssetGuid);
                ScriptGraphAsset scriptGraphAsset = AssetDatabase.LoadAssetAtPath<ScriptGraphAsset>(assetPath);
                var events = scriptGraphAsset.graph.GetUnitsRecursive(Recursion.New(100));
                if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                {
                    foreach (OnUnityEvent item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                    {
                        var type = item.UnityEvent.connection.source.type;
                        var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                        var delegateType = method?.GetParameters()[0].ParameterType;
                        types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                    }
                }
                currentProgress++;
                EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Script Graphs...", (float)currentProgress / totalProgressSteps);
            }

            var activeScenePath = SceneManager.GetActiveScene().path;

            foreach (string scenePath in scenePaths)
            {
                EditorSceneManager.OpenScene(scenePath);
                var scene = EditorSceneManager.GetSceneByPath(scenePath);

                if (!scene.IsValid()) continue;
                foreach (var gameObject in scene.GetRootGameObjects())
                {
                    if (gameObject.TryGetComponent<ScriptMachine>(out var scriptMachine))
                    {
                        if (scriptMachine != null && scriptMachine.nest.embed != null)
                        {
                            var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                            if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                            {
                                foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                                {
                                    var type = item.UnityEvent.connection.source.type;
                                    var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                    var delegateType = method?.GetParameters()[0].ParameterType;
                                    types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                                }
                            }
                        }
                    }
                    currentProgress++;
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Scenes...", (float)currentProgress / totalProgressSteps);
                }
            }
            EditorSceneManager.OpenScene(activeScenePath);
            return types;
        }

        private List<string> FindScenesInAssetsFolder()
        {
            List<string> scenePaths = new List<string>();

            string[] sceneGUIDs = AssetDatabase.FindAssets("t:Scene");

            foreach (string sceneGUID in sceneGUIDs)
            {
                string scenePath = AssetDatabase.GUIDToAssetPath(sceneGUID);

                // Check if the scene is within the Assets folder
                if (scenePath.StartsWith("Assets/"))
                {
                    scenePaths.Add(scenePath);
                }
            }

            return scenePaths;
        }
    }
}
MarkusFynd commented 4 months ago

I changed line 33 to string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs"; since it included "\nEnsure that the script stays here" in the path, which threw an exception.

I then commented out lines 153-184 because it got stuck loading the scenes, and I got an error:

ArgumentException: Scene file not found: ''.
UnityEditor.SceneManagement.EditorSceneManager.OpenScene (System.String scenePath, UnityEditor.SceneManagement.OpenSceneMode mode) (at <11d97693183d4a6bb35c29ae7882c66b>:0)
UnityEditor.SceneManagement.EditorSceneManager.OpenScene (System.String scenePath) (at <11d97693183d4a6bb35c29ae7882c66b>:0)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.FindGraphsAndScenes () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:157)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.GenerateScript () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:30)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.OnInspectorGUI () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/GenerateAotSupport.cs:22)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass59_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <b0f293012f4f4a13b3c5a7dc37df7ba8>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

In my test case, I don't have any script graphs in any scenes, only in a prefab, so I didn't need to traverse scenes. However in our real project we would like to have script graphs in scenes.

With this, the generated script is still empty, unfortunately. I've done this in both our base project where we load the addressable asset, as well as the project where we build our addressables.

I can see if I can send you my test projects, maybe that will help.

S2NX7 commented 4 months ago

I can see if I can send you my test projects, maybe that will help.

Ok sure.

S2NX7 commented 4 months ago

string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs"; since it included "\nEnsure that the script stays here" in the path, which threw an exception.

Oh I put it "\nEnsure that the script stays here" in the wrong place.

MarkusFynd commented 4 months ago

I might have to get that to you tomorrow, btw. Sorry about the delay. I might be able to do it tonight, but I can't promise that.

S2NX7 commented 4 months ago

I might have to get that to you tomorrow, btw. Sorry about the delay. I might be able to do it tonight, but I can't promise that.

no problem

S2NX7 commented 4 months ago

Also I made a change to the script to not attempt to Generate the AOT Methods for a certain scene if it can not find its path.

You can try it to see if it Solves the problem or atleast Helps.

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";
            Debug.Log("Generated OnUnityEvent Support script at : " + path + "\nEnsure that the script stays here");
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            string scriptContent = @"using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();
            string currentScene = EditorSceneManager.GetActiveScene().path;
            string[] scriptGraphAssetGuids = AssetDatabase.FindAssets($"t:{typeof(ScriptGraphAsset)}");
            var scenePaths = EditorBuildSettings.scenes.Select(scene => scene.path).ToArray();
            int totalProgressSteps = scriptGraphAssetGuids.Length + scenePaths.Length;
            int currentProgress = 0;
            int progressUpdateInterval = Math.Max(totalProgressSteps / 100, 1);

            foreach (var scriptGraphAssetGuid in scriptGraphAssetGuids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(scriptGraphAssetGuid);
                ScriptGraphAsset scriptGraphAsset = AssetDatabase.LoadAssetAtPath<ScriptGraphAsset>(assetPath);
                var events = scriptGraphAsset.graph.GetUnitsRecursive(Recursion.New(100));
                if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                {
                    foreach (OnUnityEvent item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                    {
                        var type = item.UnityEvent.connection.source.type;
                        var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                        var delegateType = method?.GetParameters()[0].ParameterType;
                        types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                    }
                }

                currentProgress++;
                if (currentProgress % progressUpdateInterval == 0)
                {
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Script Graphs...", (float)currentProgress / totalProgressSteps);
                }
            }

            foreach (string scenePath in scenePaths)
            {
                if (string.IsNullOrEmpty(scenePath))
                {
                    Debug.LogWarning($"Did not get types from some scenes. " + scenePath);
                    continue;
                }

                var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);

                foreach (var gameObject in scene.GetRootGameObjects())
                {
                    if (gameObject.TryGetComponent<ScriptMachine>(out var scriptMachine))
                    {
                        if (scriptMachine != null && scriptMachine.nest.embed != null)
                        {
                            var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                            if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                            {
                                foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                                {
                                    var type = item.UnityEvent.connection.source.type;
                                    var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                    var delegateType = method?.GetParameters()[0].ParameterType;
                                    types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                                }
                            }
                        }
                    }
                }

                currentProgress++;
                if (currentProgress % progressUpdateInterval == 0)
                {
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Scenes...", (float)currentProgress / totalProgressSteps);
                }
            }

            EditorSceneManager.OpenScene(currentScene, OpenSceneMode.Single);

            EditorUtility.ClearProgressBar();
            return types;
        }
    }
}
MarkusFynd commented 4 months ago

Here I've attached my test projects. We are using Unity 2021.3.16 for the time being. These do not include the latest update to the editor script in your previous comment. UVS-Addressables.zip I've included a readme, but feel free to ask questions if something is unclear. You'll also notice I have the UVS package in there with the modified AotPreBuilder script, as described in the OP and in this thread. Thank you again for all your help and support!

S2NX7 commented 4 months ago

Ok so from testing this fixed the problem in the bolt addressables project it was not getting the types from any prefabs so I added that:

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine.SceneManagement;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            targetObject.typesToSupport.RemoveAll(group => group.types.Count == 0);
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";
            Debug.Log("Generated OnUnityEvent Support script at : " + path + "\nEnsure that the script stays here");
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            string scriptContent = @"using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();
            string currentScene = EditorSceneManager.GetActiveScene().path;
            string[] scriptGraphAssetGuids = AssetDatabase.FindAssets($"t:{typeof(ScriptGraphAsset)}");
            var scenePaths = EditorBuildSettings.scenes.Select(scene => scene.path).ToArray();
            string[] prefabGuids = AssetDatabase.FindAssets("t:Prefab");
            int totalProgressSteps = scriptGraphAssetGuids.Length + scenePaths.Length + prefabGuids.Length;
            int currentProgress = 0;
            int progressUpdateInterval = Math.Max(totalProgressSteps / 100, 1);

            foreach (var scriptGraphAssetGuid in scriptGraphAssetGuids)
            {
                string assetPath = AssetDatabase.GUIDToAssetPath(scriptGraphAssetGuid);
                ScriptGraphAsset scriptGraphAsset = AssetDatabase.LoadAssetAtPath<ScriptGraphAsset>(assetPath);
                var events = scriptGraphAsset.graph.GetUnitsRecursive(Recursion.New(100));
                if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                {
                    foreach (OnUnityEvent item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                    {
                        var type = item.UnityEvent.connection.source.type;
                        var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                        var delegateType = method?.GetParameters()[0].ParameterType;
                        types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                    }
                }

                currentProgress++;
                if (currentProgress % progressUpdateInterval == 0)
                {
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Script Graphs...", (float)currentProgress / totalProgressSteps);
                }
            }

            foreach (string scenePath in scenePaths)
            {
                if (string.IsNullOrEmpty(scenePath))
                {
                    Debug.LogWarning($"Did not get types from some scenes. " + scenePath);
                    continue;
                }

                var scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single);

                foreach (var gameObject in scene.GetRootGameObjects())
                {
                    if (gameObject.TryGetComponent<ScriptMachine>(out var scriptMachine))
                    {
                        if (scriptMachine != null && scriptMachine.nest.embed != null)
                        {
                            var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                            if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                            {
                                foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                                {
                                    var type = item.UnityEvent.connection.source.type;
                                    var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                    var delegateType = method?.GetParameters()[0].ParameterType;
                                    types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                                }
                            }
                        }
                    }
                }

                currentProgress++;
                if (currentProgress % progressUpdateInterval == 0)
                {
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Scenes...", (float)currentProgress / totalProgressSteps);
                }
            }

            foreach (var prefabGuid in prefabGuids)
            {
                string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid);
                if(!prefabPath.StartsWith("Assets/")) continue;
                GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(prefabPath);
                if (prefab != null)
                {
                    var scriptMachines = prefab.GetComponentsInChildren<ScriptMachine>(true).Where(machine => machine.nest.embed != null).ToList();
                    scriptMachines.AddRange(prefab.GetComponents<ScriptMachine>().Where(machine => machine.nest.embed != null));
                    foreach (var scriptMachine in scriptMachines)
                    {
                        var events = scriptMachine.nest.graph.GetUnitsRecursive(Recursion.New(100));
                        if (events.Any(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection))
                        {
                            foreach (var item in events.Where(unit => unit is OnUnityEvent _event && _event.UnityEvent.hasValidConnection).Cast<OnUnityEvent>())
                            {
                                var type = item.UnityEvent.connection.source.type;
                                var method = type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
                                var delegateType = method?.GetParameters()[0].ParameterType;
                                types.Add(new OnUnityEventsAotSupport.TypeGroup(delegateType.GetGenericArguments()));
                            }
                        }
                    }
                }

                currentProgress++;
                if (currentProgress % progressUpdateInterval == 0)
                {
                    EditorUtility.DisplayProgressBar("Generating Aot Support", "Processing Prefabs...", (float)currentProgress / totalProgressSteps);
                }
            }

            EditorSceneManager.OpenScene(currentScene, OpenSceneMode.Single);

            EditorUtility.ClearProgressBar();
            return types;
        }
    }
}
MarkusFynd commented 4 months ago

In AddressableLoadTest, I can't find any graphs anywhere in the project

Right, that is because the goal of my testing is to support loading prefabs via Addressables that has embedded logic. This is why we want to generate AOT stubs for the UVS nodes, including the Community Addon nodes. The base project doesn't have any graphs, although we could include a "super graph" that just includes the needed nodes, but ideally we want to be able to generate AOT stubs programmatically instead of managing a huge graph with potentially lots and lots of nodes šŸ˜…

MarkusFynd commented 4 months ago

With the updated Editor script, it seems I'm still getting the same error:

ArgumentException: Scene file not found: ''.
UnityEditor.SceneManagement.EditorSceneManager.OpenScene (System.String scenePath, UnityEditor.SceneManagement.OpenSceneMode mode) (at <11d97693183d4a6bb35c29ae7882c66b>:0)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.FindGraphsAndScenes () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:231)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.GenerateScript () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:30)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.OnInspectorGUI () (at Packages/dev.bolt.addons@f8d1c5e2b1/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:22)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass59_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <b0f293012f4f4a13b3c5a7dc37df7ba8>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)

Then Unity gets stuck on an "Importing assets" progress bar that doesn't go away. Did you change anything else in the project that I need to update?

S2NX7 commented 4 months ago

Then Unity gets stuck on an "Importing assets" progress bar that doesn't go away. Did you change anything else in the project that I need to update?

No I did not. It did show me this aswell at first but then stopped. Maybe you can try and put the GenerateAotSupport script in a editor folder in the Assets Folder and just to make sure you can open the Build settings window.

MarkusFynd commented 4 months ago

Ah, got it. It was because I didn't have any scenes in build settings. The generated AotSupportMethods script has content now. Will test some more.

MarkusFynd commented 4 months ago

Alright so what I've done now, is I generated the AOT support in the addressable project (BoltAddressables). I then copied the generated cs-file to the same folder in the base project (AddressablesLoadTest), so that they are identical. When loading the addressables prefab on Windows (editor), it works. However in an Android build, I am getting the following error:

InvalidOperationException: No AotSupportMethods script Found
  at Unity.VisualScripting.Community.OnUnityEvent.CreateAction (System.Type delegateType, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.Community.OnUnityEvent.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.FlowGraph.StartListening (Unity.VisualScripting.GraphStack stack) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.XGraphEventListener.StartListening (Unity.VisualScripting.IGraphEventListener listener, Unity.VisualScripting.GraphReference reference) [0x00000] in <00000000000000000000000000000000>:0 
  at Unity.VisualScripting.ScriptMachine.OnEnable () [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.Object.Instantiate[T] (T original) [0x00000] in <00000000000000000000000000000000>:0 
  at UnityEngine.ResourceManagement.ResourceProvide

I believe I've seen this error before.

As mentioned, the goal is to support logic on assets loaded via addressables, so the base project (AddressablesLoadTest) isn't supposed to have any graphs. However, for good measure I made a simple script graph in the base project that does the same thing as the one I'm trying to load, with the Slider OnValueChanged event, and I still got the same error even when it's part of the base project.

Edit: the working OnUnityEvent with no parameters also stopped working again.

S2NX7 commented 4 months ago

Ok you can try and change the OnUnityEvent script to this :

using System;
using System.IO;
using System.Linq;
using System.Reflection;
using UnityEngine;
using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{

    internal class OnUnityEventData : EventUnit<EventData>.Data
    {
        public object EventListener { get; set; }
    }

    public class EventData
    {
        public object Value0 { get; set; }
        public object Value1 { get; set; }
        public object Value2 { get; set; }
        public object Value3 { get; set; }
    }

    [UnitTitle("On Unity Event")]
    [UnitCategory("Events")]
    public class OnUnityEvent : EventUnit<EventData>
    {
        protected override bool register => false;

        [DoNotSerialize]
        public ValueInput UnityEvent;

        public Type Type { get; private set; }

        public override IGraphElementData CreateData()
        {
            return new OnUnityEventData();
        }

        protected override void Definition()
        {
            base.Definition();

            UnityEvent = ValueInput<UnityEventBase>("event");

            if (Type != null)
            {
                var genericArguments = Type.GetGenericArguments();
                for (var i = 0; i < genericArguments.Length; i++)
                {
                    ValueOutput(genericArguments[i], $"arg{i}");
                }
            }
        }

        public override void StartListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener != null || !UnityEvent.hasValidConnection) return;

            UpdatePorts();

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.AddListener));
            var delegateType = method?.GetParameters()[0].ParameterType;

            data.EventListener = CreateAction(delegateType, stackRef);

            method?.Invoke(eventBase, new[] { data.EventListener });
        }

        public override void StopListening(GraphStack stack)
        {
            var data = GetData(stack);

            if (data.EventListener == null) return;

            var stackRef = stack.ToReference();
            var eventBase = Flow.FetchValue<UnityEventBase>(UnityEvent, stackRef);
            var method = Type.GetMethod(nameof(UnityEngine.Events.UnityEvent.RemoveListener));
            method?.Invoke(eventBase, new[] { data.EventListener });

            data.EventListener = null;
        }

        public void UpdatePorts()
        {
            Type = GetEventType();
            Define();
        }

        private Type GetEventType()
        {
            var eventType = UnityEvent?.connection?.source?.type;

            while (eventType != null && eventType.BaseType != typeof(UnityEventBase))
            {
                eventType = eventType.BaseType;
            }

            return eventType;
        }

        private object CreateAction(Type delegateType, GraphReference reference)
        {
            var numParams = delegateType.GetGenericArguments().Length;

            if (numParams == 0)
            {
                void Action()
                {
                    Trigger(reference, new EventData());
                }
                return (UnityAction)Action;
            }

            if (GetAotSupportMethodsType() != null)
            {
                Type aotSupportMethodsType = GetAotSupportMethodsType();
                var method = aotSupportMethodsType.GetMethods().First(method => method.ReturnType == delegateType);
                return method?.Invoke(null, new object[] { reference, this });
            }
            else
            {
                throw new InvalidOperationException("Could not find AOT Support Methods");
            }

        }

        internal UnityAction<T1> OneParamHandler<T1>(GraphReference reference)
        {
            return arg0 =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0
                });
            };
        }

        internal UnityAction<T1, T2> TwoParamsHandler<T1, T2>(GraphReference reference)
        {
            return (arg0, arg1) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1
                });
            };
        }

        internal UnityAction<T1, T2, T3> ThreeParamsHandler<T1, T2, T3>(GraphReference reference)
        {
            return (arg0, arg1, arg2) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2
                });
            };
        }

        internal UnityAction<T1, T2, T3, T4> FourParamsHandler<T1, T2, T3, T4>(GraphReference reference)
        {
            return (arg0, arg1, arg2, arg3) =>
            {
                Trigger(reference, new EventData
                {
                    Value0 = arg0,
                    Value1 = arg1,
                    Value2 = arg2,
                    Value3 = arg3
                });
            };
        }

        protected override void AssignArguments(Flow flow, EventData args)
        {
            var numOutputs = valueOutputs.Count;

            if (numOutputs > 0) flow.SetValue(valueOutputs[0], args.Value0);
            if (numOutputs > 1) flow.SetValue(valueOutputs[1], args.Value1);
            if (numOutputs > 2) flow.SetValue(valueOutputs[2], args.Value2);
            if (numOutputs > 3) flow.SetValue(valueOutputs[3], args.Value3);
        }

        private OnUnityEventData GetData(GraphPointer stack)
        {
            return stack.GetElementData<OnUnityEventData>(this);
        }

        private Type GetAotSupportMethodsType()
        {
            Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();

            foreach (Assembly assembly in assemblies)
            {
                Type[] types = assembly.GetTypes();

                foreach (Type type in types)
                {
                    if (type.Name == "AotSupportMethods")
                    {
                        return type;
                    }
                }
            }
            return null;
        }

    }
}
MarkusFynd commented 4 months ago

Hooray, it works!! I am now able to load an addressable asset with logic on Android. You've really been a huge help, so thank you again.

I guess my big question next is, is it possible to generate an AotSupportMethods for all potential nodes? Or somehow include them in the base project, without the need for it to either have its own script graphs, or copy the generated script over from the addressables project. Ideally, we want this to happen as part of the build process when we build the base project. That was the initial goal with the modified AotPreBuilder script, so that we would get AOT-stubs for UVS nodes.

S2NX7 commented 4 months ago

Hooray, it works!! I am now able to load an addressable asset with logic on Android. You've really been a huge help, so thank you again.

I guess my big question next is, is it possible to generate an AotSupportMethods for all potential nodes? Or somehow include them in the base project, without the need for it to either have its own script graphs, or copy the generated script over from the addressables project. Ideally, we want this to happen as part of the build process when we build the base project. That was the initial goal with the modified AotPreBuilder script, so that we would get AOT-stubs for UVS nodes.

are you asking is there a way to generate the Support Methods when you build the game?

S2NX7 commented 4 months ago

I guess my big question next is, is it possible to generate an AotSupportMethods for all potential nodes?

What do you mean by this?

Like all OnUnityEvent nodes?

S2NX7 commented 4 months ago

I see here there is a script that has a variable called catalogURL. You want to use something similar to get the script machines from Addressables?

MarkusFynd commented 4 months ago

So in our workflow, we have one base project that holds the main logic of the game, but almost no 3D assets. We've come up with a system to load all our assets on demand using the addressable system.

I see here there is a script that has a variable called catalogURL. You want to use something similar to get the script machines from Addressables?

Yes, pretty much this. The test projects I sent you are very small example projects, our real project has a lot more stuff going on, but the idea is similar.

  1. Load in the main game with almost no visual assets
  2. Create scenes and prefabs for 3D assets in separate projects, and build them as addressables
  3. Upload the built addressable bundles to our server
  4. Download 3D assets to the main game on demand from our server, and instantiate them using the addressable system

We want to support having UVS logic in our addressable assets, but since we target Android we need to generate AOT code for all logic - we cannot support UVS logic the base game "doesn't know about". This is why we started pursuing generating AOT stubs for UVS nodes with the modified AotPreBuilder script as described in the the thread linked in the OP. We came pretty far by doing that and had some success with the default UVS nodes, however the Community Addon nodes (OnUnityEvent) was still not getting AOT stubs generated.

What do you mean by this? Like all OnUnityEvent nodes?

are you asking is there a way to generate the Support Methods when you build the game?

So the best case scenario, if possible, would be that when we build our base game, a script looks at available visual scripting nodes (not just the ones being used), not just OnUnityEvent nodes, and generates AOT stubs for all of them so that if an addressable asset with UVS logic is instantiated into the game in runtime, it will still work on Android. We realize this isn't a small feat, but with your help over the course of this thread we've been able to make a proof of concept at the very least! Since we have several different addressable bundles that are loaded into the game on demand, having to include the generated AotSupportMethods script in the main game is a bit clunky since we'd have to do that for potentially many addressable asset projects.

Think of the addressable bundles like DLC's - we basically want to support UVS logic in DLC's without having to ship a new build of the main game.

S2NX7 commented 4 months ago

Ok I understand now thanks.

Are you able to show me how you get/load these assets into your game.

S2NX7 commented 4 months ago

Because then I can attempt to get the assets that have script machines attached and then look through the graphs to find any nodes that need AOT methods

MarkusFynd commented 4 months ago

Are you able to show me how you get/load these assets into your game.

The example script SpawnAddressableAsset in the project I sent you basically is what we do. Here is another simplified example:

private static string catalogURL = "https://url/to/server/Android/catalog.json";
private IEnumerator Start()
{
    var catalogHandle = Addressables.LoadContentCatalogAsync(catalogURL, false);
    yield return catalogHandle;
    if (catalogHandle.Status != AsyncOperationStatus.Succeeded)
    {
        Debug.Log("Failed getting addressables catalog");
        yield break;
    }

    const string key = "AddressableTest/Prefabs/UVSTestPrefab";
    var prefabHandle = Addressables.InstantiateAsync(key);
    yield return prefabHandle;
    if (prefabHandle.Status != AsyncOperationStatus.Succeeded)
        Debug.Log("Failed spawning addressable prefab");
}

This is happening in the base game, so at this point the AOT methods need to already have been generated. A sort of mental model we have regarding the addressable system, is that the base game's project and the addressable asset projects needs to have the same scripts, so that Unity can serialize them when being built and deserialize them in the base game. The addressable being loaded in expects there to be some scripts in the base game for the UVS logic to work.

This is why we want the base game to generate AOT code for all UVS nodes, so that if an addressable prefab is loaded in to the game expecting that code to be there, it will work.

MarkusFynd commented 4 months ago

I realize our use case is a bit unusual - most people are probably building games with UVS logic in the base game, so when it's built for Android it works because the AOT methods would be in the same game.

Since we're trying to support UVS logic in DLC's built in separate projects, the base game doesn't know what UVS nodes might be part of those assets, which is why we want to generate AOT code for all potential nodes.

S2NX7 commented 4 months ago

well I can try to experiment around to see if I can come up with a solution to this.

MarkusFynd commented 4 months ago

Just to reiterate what we were trying to achieve in the first place with the modified AotPreBuilder script in the UVS package, there's a custom FindAllAdditionalStubs() method that looks for additional logic to create AOT code for. The _allowedNamespaces HashSet in the same script includes namespaces we want to include in the AOT build, so the first thing we tried was adding Unity.VisualScripting.Community to that list - it didn't work, but if it was that simple that would solve everything for us. Then, during the build process of the main game, we'd get all the AOT code needed for potential DLC's with logic.

So the absolute ideal solution is to have some extra scripts in the Community Addons package that generates the AOT code on build, or letting the AotPreBuilder script do it somehow.

S2NX7 commented 4 months ago

Just to reiterate what we were trying to achieve in the first place with the modified AotPreBuilder script in the UVS package, there's a custom FindAllAdditionalStubs() method that looks for additional logic to create AOT code for. The _allowedNamespaces HashSet in the same script includes namespaces we want to include in the AOT build, so the first thing we tried was adding Unity.VisualScripting.Community to that list - it didn't work, but if it was that simple that would solve everything for us. Then, during the build process of the main game, we'd get all the AOT code needed for potential DLC's with logic.

So the absolute ideal solution is to have some extra scripts in the Community Addons package that generates the AOT code on build.

The reason it's not working when you add Unity.VisualScripting.Community Is because the node is not in the project when you build it so it does not create a method with the event parameter types so what I did is create a script to do this now I just need to figure out how to get those graphs from your addressesables and create methods for any node that needs it.

MarkusFynd commented 4 months ago

because the node is not in the project when you build it so it does not create a method with the event parameter types

Right, that makes sense. That's why we wanted to generate the AOT code for all potential nodes šŸ˜…

I don't know if it's possible to get script graphs from the addressables themselves. The URL's for the bundles are not always known until runtime, so the AOT code should be generated beforehand - I don't think we can get the addressables with graphs at build time.

S2NX7 commented 4 months ago

because the node is not in the project when you build it so it does not create a method with the event parameter types

Right, that makes sense. That's why we wanted to generate the AOT code for all potential nodes šŸ˜…

I don't know if it's possible to get script graphs from the addressables themselves. The URL's for the bundles are not always known until runtime, so the AOT code should be generated beforehand - I don't think we can get the addressables with graphs at build time.

Ok so here this should generate methods for all types that are usable by the OnUnityEvent this is the only other solution i could think of:

using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System;
using System.IO;
using Unity.VisualScripting.Community.Libraries.Humility;
using System.Linq;
using UnityEditor.SceneManagement;
using UnityEngine.Events;

namespace Unity.VisualScripting.Community
{
    [CustomEditor(typeof(OnUnityEventsAotSupport))]
    public class OnUnityEventsAotSupportEditor : Editor
    {
        public int callbackOrder => 0;

        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            if (GUILayout.Button("Generate Aot Support"))
            {
                GenerateScript();
            }
        }

        private void GenerateScript()
        {
            EditorUtility.DisplayProgressBar("Generating Aot Support", "Finding Script Graphs and Scenes...", 0f);
            OnUnityEventsAotSupport targetObject = (OnUnityEventsAotSupport)target;
            targetObject.typesToSupport = FindGraphsAndScenes().Distinct().ToList();
            targetObject.typesToSupport.RemoveAll(type => type.types.Count == 0);
            List<OnUnityEventsAotSupport.TypeGroup> typeGroups = targetObject.typesToSupport;
            string scriptContent = GenerateScriptContent(typeGroups);
            string path = Application.dataPath + "/Unity.VisualScripting.Community.Generated/Scripts/AotSupportMethods.cs";
            Debug.Log("Generated OnUnityEvent Support script at : " + path + "\nEnsure that the script stays here");
            HUMIO.Ensure(path).Path();
            File.WriteAllText(path, scriptContent);
            AssetDatabase.Refresh();
            EditorUtility.ClearProgressBar();
        }

        private string GenerateScriptContent(List<OnUnityEventsAotSupport.TypeGroup> typeGroups)
        {
            HashSet<string> namespaces = new HashSet<string>();

            foreach (var typeGroup in typeGroups)
            {
                foreach (var type in typeGroup.types)
                {
                    string namespaceName = type.Namespace;
                    if (!string.IsNullOrEmpty(namespaceName) && !IsIncludedNamespace(namespaceName) && AllowedNameSpace(namespaceName))
                    {
                        namespaces.Add(namespaceName);
                    }
                }

            }

            string namespaceDeclarations = string.Join(Environment.NewLine, namespaces.Select(ns => $"using {ns};"));
            string scriptContent = $"{namespaceDeclarations}\n";
            scriptContent += @"using System;
using UnityEngine.Events;
using Unity.VisualScripting;
using Unity.VisualScripting.Community;

public static class AotSupportMethods
{
";

            foreach (var typeGroup in typeGroups)
            {
                string methodParameters = string.Join(", ", typeGroup.types.Select(type => CSharpName(type, true)));

                string methodReturnType = "UnityAction<" + methodParameters + ">";

                scriptContent += $"    public static {methodReturnType} {string.Join("_", typeGroup.types.Select(type => CSharpName(type, false)))}Handler(GraphReference reference, OnUnityEvent onUnityEvent)\n";
                scriptContent += "    {\n";

                // Construct argument list
                List<string> args = new List<string>();
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    args.Add("arg" + i);
                }

                // Construct event data initialization
                string eventData = "";
                for (int i = 0; i < typeGroup.types.Count; i++)
                {
                    eventData += $"Value{i} = arg{i},{(i != typeGroup.types.Count - 1 ? "\n" : "")}\t\t\t\t";
                }

                // Append method body
                scriptContent += @$"        return ({string.Join(", ", args)}) => 
        {{
            onUnityEvent.Trigger(reference, new EventData
            {{  
                {eventData}
            }});
        }};
";
                scriptContent += "    }\n\n";
            }

            scriptContent += "}\n";

            return scriptContent;
        }

        private string CSharpName(Type type, bool lowerCase)
        {
            if (lowerCase)
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "int";
                if (type == typeof(string)) return "string";
                if (type == typeof(float)) return "float";
                if (type == typeof(double)) return "double";
                if (type == typeof(bool)) return "bool";
                if (type == typeof(byte)) return "byte";
                if (type == typeof(object) && type.BaseType == null) return "object";
                if (type == typeof(object[])) return "object[]";

                return type.Name;
            }
            else
            {
                if (type == null) return "null";
                if (type == typeof(int)) return "Int";
                if (type == typeof(string)) return "String";
                if (type == typeof(float)) return "Float";
                if (type == typeof(double)) return "Double";
                if (type == typeof(bool)) return "Bool";
                if (type == typeof(byte)) return "Byte";
                if (type == typeof(object) && type.BaseType == null) return "Object";
                if (type == typeof(object[])) return "ObjectArray";

                return string.Concat(type.Name.Split('.').Select(word => char.ToUpper(word[0]) + word.Substring(1)));
            }
        }

        public List<OnUnityEventsAotSupport.TypeGroup> FindGraphsAndScenes()
        {
            List<OnUnityEventsAotSupport.TypeGroup> types = new List<OnUnityEventsAotSupport.TypeGroup>();

            foreach (var type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(assembly => assembly.GetTypes().Where(type => type.IsSubclassOf(typeof(UnityEventBase)))))
            {
                if (type.BaseType.IsGenericType && !type.BaseType.GetGenericArguments().Any(arg => arg.IsGenericTypeParameter) && type.BaseType.GetGenericArguments().All(_type => _type.IsPublic && AllowedNameSpace(_type.Namespace)))
                {
                    types.Add(new OnUnityEventsAotSupport.TypeGroup(type.BaseType.GetGenericArguments()));
                }
            }

            return types;
        }

        private bool IsIncludedNamespace(string _namespace)
        {
            return _namespace == "System" || _namespace == "UnityEngine.Events" || _namespace == "Unity.VisualScripting" || _namespace == "Unity.VisualScripting.Community";
        }

        private bool AllowedNameSpace(string _namespace)
        {
            if(_namespace.Contains("UnityEditor") || _namespace.Contains("NUnit")) return false;
            return true;
        }
    }
}
MarkusFynd commented 4 months ago

Nice! The first version, before you edited your comment, seems to almost work. I got some unexpected character errors, and an Cannot resolve symbol 'NUnit' error.

After your edit, I get this null ref:

NullReferenceException: Object reference not set to an instance of an object
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.AllowedNameSpace (System.String _namespace) (at Packages/dev.bolt.addons@2c3e6b14bb/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:167)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.<FindGraphsAndScenes>b__6_3 (System.Type _type) (at Packages/dev.bolt.addons@2c3e6b14bb/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:151)
System.Linq.Enumerable.All[TSource] (System.Collections.Generic.IEnumerable`1[T] source, System.Func`2[T,TResult] predicate) (at <1c318258bf0843289b0e2cbe692fee39>:0)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.FindGraphsAndScenes () (at Packages/dev.bolt.addons@2c3e6b14bb/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:151)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.GenerateScript () (at Packages/dev.bolt.addons@2c3e6b14bb/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:32)
Unity.VisualScripting.Community.OnUnityEventsAotSupportEditor.OnInspectorGUI () (at Packages/dev.bolt.addons@2c3e6b14bb/Editor/Code/Generators/OnUnityEventsAotSupportEditor.cs:24)
UnityEditor.UIElements.InspectorElement+<>c__DisplayClass59_0.<CreateIMGUIInspectorFromEditor>b__0 () (at <b0f293012f4f4a13b3c5a7dc37df7ba8>:0)
UnityEngine.GUIUtility:ProcessEvent(Int32, IntPtr, Boolean&)
MarkusFynd commented 4 months ago

As of now, we mainly need the OnUnityEvent node, and haven't done extensive testing with other Community Addon-nodes. But if we come across this error with other nodes, will there be a similar solution that can be done you think? The best case scenario would be to generate AOT support methods for all potential nodes, or at least most nodes. If we can't support all possible nodes, supporting the nodes that are most likely to be used would be awesome. I'm not sure which other nodes that might apply to, as we haven't done lots of testing, but if the issue should arise it would be great to have a solution.