diff --git a/Docs/flow_startup.md b/Docs/flow_startup.md index 101ce59..3409421 100644 --- a/Docs/flow_startup.md +++ b/Docs/flow_startup.md @@ -98,12 +98,14 @@ public class MyComponent: Component } ``` -2. For static method, create a new class and implement `ExecutableFunctionLibrary` to - add static executable functions, then add `ExecutableFunctionAttribute`. +2. For static method, create a new partial class and implement `ExecutableFunctionLibrary` to + add static executable functions, then add `ExecutableFunctionAttribute`. + + >You must add `partial` modifier to let source generator work. Source generator will register static function pointer to the flow reflection system instead of using MethodInfo to enhance runtime performance. ```C# -public class UnityExecutableFunctionLibrary: ExecutableFunctionLibrary +public partial class UnityExecutableFunctionLibrary: ExecutableFunctionLibrary { // IsScriptMethod will consider UObject as function target type // IsSelfTarget will let graph pass self reference as first parameter if self is UObject @@ -472,6 +474,49 @@ public void Test() } ``` +#### Source Generator + +In [executable function part](#executable-function), it is mentioned that source generator will register static methods to improve runtime performance. + +The following shows what SourceGenerator does. + +Source code: + +```C# +/// +/// Executable function library for ceres +/// +[CeresGroup("Ceres")] +public partial class CeresExecutableLibrary: ExecutableFunctionLibrary +{ + [ExecutableFunction, CeresLabel("Set LogLevel")] + public static void Flow_SetLogLevel(LogType logType) + { + CeresAPI.LogLevel = logType; + } + + [ExecutableFunction(ExecuteInDependency = true), CeresLabel("Get LogLevel")] + public static LogType Flow_GetLogLevel() + { + return CeresAPI.LogLevel; + } +} +``` + +Generated code: + +```C# +[CompilerGenerated] +public partial class CeresExecutableLibrary +{ + protected override unsafe void CollectExecutableFunctions() + { + RegisterExecutableFunctions(nameof(Flow_SetLogLevel), 1, (delegate* )&Flow_SetLogLevel); + RegisterExecutableFunctions(nameof(Flow_GetLogLevel), 0, (delegate* )&Flow_GetLogLevel); + } +} +``` + ## Debug To enable and disable debug mode, click `debug` button in the upper right corner. diff --git a/Runtime/Core/Components/SceneVariableScope.cs b/Runtime/Core/Components/SceneVariableScope.cs index 8e55c47..0f2da30 100644 --- a/Runtime/Core/Components/SceneVariableScope.cs +++ b/Runtime/Core/Components/SceneVariableScope.cs @@ -6,21 +6,27 @@ public class SceneVariableScope : MonoBehaviour, IVariableScope, IVariableSource { [SerializeReference] private List sharedVariables = new(); + public List SharedVariables => sharedVariables; + [SerializeField] private GameVariableScope parentScope; + public GlobalVariables GlobalVariables { get; private set; } - private bool initialized = false; + + private bool _initialized; + private void Awake() { - if (!initialized) + if (!_initialized) { Initialize(); } } + public void Initialize() { - initialized = true; + _initialized = true; if (parentScope && parentScope.IsCurrentScope()) { GlobalVariables = new GlobalVariables(sharedVariables, parentScope); @@ -30,6 +36,7 @@ public void Initialize() GlobalVariables = new GlobalVariables(sharedVariables); } } + private void OnDestroy() { GlobalVariables.Dispose(); diff --git a/Runtime/Core/Models/CeresAPI.cs b/Runtime/Core/Models/CeresAPI.cs index bd049b7..3d8654f 100644 --- a/Runtime/Core/Models/CeresAPI.cs +++ b/Runtime/Core/Models/CeresAPI.cs @@ -11,6 +11,11 @@ public static class CeresAPI /// public static LogType LogLevel { get; set; } = LogType.Log; + /// + /// Whether to log relink details + /// + public static bool LogUObjectRelink { get; set; } + public static void LogWarning(string message) { if(LogLevel >= LogType.Warning) diff --git a/Runtime/Core/Models/GlobalVariables.cs b/Runtime/Core/Models/GlobalVariables.cs index 7c0abd6..90c69c4 100644 --- a/Runtime/Core/Models/GlobalVariables.cs +++ b/Runtime/Core/Models/GlobalVariables.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; -using UnityEngine; -using Object = UnityEngine.Object; +using UObject = UnityEngine.Object; namespace Ceres { /// @@ -11,46 +10,53 @@ namespace Ceres public class GlobalVariables : IVariableSource, IDisposable { public List SharedVariables { get; } - private static GlobalVariables instance; - public static GlobalVariables Instance => instance ?? FindOrCreateDefault(); - private readonly IVariableScope parentScope; + + private static GlobalVariables _instance; + + public static GlobalVariables Instance => _instance ?? FindOrCreateDefault(); + + private readonly IVariableScope _parentScope; + public GlobalVariables(List sharedVariables) { - instance = this; + _instance = this; SharedVariables = new List(sharedVariables); } + public GlobalVariables(List sharedVariables, IVariableScope parentScope) { - instance = this; - this.parentScope = parentScope; + _instance = this; + _parentScope = parentScope; SharedVariables = new List(sharedVariables); if (parentScope != null) { sharedVariables.AddRange(parentScope.GlobalVariables.SharedVariables); } } + private static GlobalVariables FindOrCreateDefault() { - var scope = Object.FindObjectOfType(); + var scope = UObject.FindObjectOfType(); if (scope != null) { scope.Initialize(); return scope.GlobalVariables; } - instance = new(new()); - return instance; + _instance = new GlobalVariables(new List()); + return _instance; } + public void Dispose() { - if (instance != this) + if (_instance != this) { - Debug.LogWarning("Only scope current used should be disposed!"); + CeresAPI.LogWarning("Global variables can only be disposed in top level scope"); return; } - instance = null; - if (parentScope != null) + _instance = null; + if (_parentScope != null) { - instance = parentScope.GlobalVariables; + _instance = _parentScope.GlobalVariables; } } } diff --git a/Runtime/Core/Models/Graph/CeresGraph.cs b/Runtime/Core/Models/Graph/CeresGraph.cs index 051b351..ad769e1 100644 --- a/Runtime/Core/Models/Graph/CeresGraph.cs +++ b/Runtime/Core/Models/Graph/CeresGraph.cs @@ -1,8 +1,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; #if CERES_DISABLE_ILPP +using Ceres.Utilities; +using System.Reflection; using Chris; using System.Collections; #endif @@ -81,9 +82,13 @@ public BlackBoard BlackBoard private readonly HashSet _internalVariables = new(); + private readonly HashSet _internalPorts = new(); + +#if CERES_DISABLE_ILPP private static readonly Dictionary> VariableFieldInfoLookup = new(); private static readonly Dictionary> PortFieldInfoLookup = new(); +#endif [SerializeReference] public List variables; @@ -142,8 +147,8 @@ protected static void InitVariables_Imp(CeresGraph graph) var internalVariables = graph._internalVariables; foreach (var node in graph.GetAllNodes()) { - /* Variables will be collected by node using ILPP */ #if !CERES_DISABLE_ILPP + /* Variables will be collected by node using ILPP */ node.InitializeVariables(); foreach (var variable in node.SharedVariables) { @@ -156,7 +161,7 @@ protected static void InitVariables_Imp(CeresGraph graph) { fields = nodeType .GetAllFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(x => x.FieldType.IsSubclassOf(typeof(SharedVariable)) || IsIListVariable(x.FieldType)) + .Where(x => x.FieldType.IsSubclassOf(typeof(SharedVariable)) || x.FieldType.IsIListVariable()) .ToList(); VariableFieldInfoLookup.Add(nodeType, fields); } @@ -187,47 +192,29 @@ protected static void InitVariables_Imp(CeresGraph graph) } } - private static bool IsIListVariable(Type fieldType) - { - if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) - { - var genericArgument = fieldType.GetGenericArguments()[0]; - if (typeof(SharedVariable).IsAssignableFrom(genericArgument)) - { - return true; - } - } - else if (fieldType.IsArray) - { - var elementType = fieldType.GetElementType(); - if (typeof(SharedVariable).IsAssignableFrom(elementType)) - { - return true; - } - } - return false; - } - /// /// Traverse the graph and init all ports automatically /// /// protected static void InitPorts_Imp(CeresGraph graph) { + var internalPorts = graph._internalPorts; foreach (var node in graph.GetAllNodes()) { - /* Ports will be collected by node using ILPP */ #if !CERES_DISABLE_ILPP + /* Ports will be collected by node using ILPP */ node.InitializePorts(); foreach (var pair in node.Ports) { graph.LinkPort(pair.Value, pair.Key, node); + internalPorts.Add(pair.Value); } foreach (var pair in node.PortLists) { for(int i = 0; i < pair.Value.Count; i++) { graph.LinkPort((CeresPort)pair.Value[i], pair.Key, i, node); + internalPorts.Add((CeresPort)pair.Value[i]); } } #else @@ -236,7 +223,7 @@ protected static void InitPorts_Imp(CeresGraph graph) { fields = nodeType .GetAllFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) - .Where(x => x.FieldType.IsSubclassOf(typeof(CeresPort)) || IsIListPort(x.FieldType)) + .Where(x => x.FieldType.IsSubclassOf(typeof(CeresPort)) || x.FieldType.IsIListPort()) .ToList(); PortFieldInfoLookup.Add(nodeType, fields); } @@ -324,27 +311,6 @@ protected virtual void LinkPort(CeresPort port, CeresNode ownerNode, CeresPortDa targetNode.NodeData.AddDependency(ownerNode.Guid); } } - - private static bool IsIListPort(Type fieldType) - { - if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) - { - var genericArgument = fieldType.GetGenericArguments()[0]; - if (typeof(CeresPort).IsAssignableFrom(genericArgument)) - { - return true; - } - } - else if (fieldType.IsArray) - { - var elementType = fieldType.GetElementType(); - if (typeof(CeresPort).IsAssignableFrom(elementType)) - { - return true; - } - } - return false; - } protected static void CollectDependencyPath(CeresGraph graph) { @@ -417,23 +383,28 @@ public virtual void Dispose() { foreach (var variable in variables) { - variable.Unbind(); + variable.Dispose(); } + variables.Clear(); foreach (var variable in _internalVariables) { - variable.Unbind(); + variable.Dispose(); } + _internalVariables.Clear(); + foreach (var port in _internalPorts) + { + port.Dispose(); + } + _internalPorts.Clear(); _nodeDependencyPath = null; - variables.Clear(); - _internalVariables.Clear(); foreach (var node in GetAllNodes()) { node.Dispose(); } nodes.Clear(); - if(_disposables != null) + if (_disposables != null) { foreach (var disposable in _disposables) { @@ -482,7 +453,7 @@ void VisitDependency(int destinationIndex, CeresNode current) foreach (var dependency in dependencies) { var dependencyNode = graph.FindNode(dependency); - if(dependencyNode == null || dependencyNode.NodeData.executionPath == ExecutionPath.Forward) + if (dependencyNode == null || dependencyNode.NodeData.executionPath == ExecutionPath.Forward) { continue; } diff --git a/Runtime/Core/Models/Graph/Nodes/CeresNode.cs b/Runtime/Core/Models/Graph/Nodes/CeresNode.cs index 33eba47..7ffae9a 100644 --- a/Runtime/Core/Models/Graph/Nodes/CeresNode.cs +++ b/Runtime/Core/Models/Graph/Nodes/CeresNode.cs @@ -388,8 +388,6 @@ public readonly override string ToString() [SerializeField] private UObjectLink[] uobjectLinks = Array.Empty(); - public static bool LogUObjectRelink { get; set; } - public void AddPortData(CeresPortData data) { ArrayUtils.Add(ref portData, data); @@ -456,7 +454,7 @@ public virtual CeresNodeData Clone() public virtual void Serialize(CeresNode node) { var type = node.GetType(); - if(type.IsGenericType) + if (type.IsGenericType) { nodeType = new NodeType(type.GetGenericTypeDefinition()); genericParameters = type.GetGenericArguments().Select(SerializedType.ToString).ToArray(); @@ -468,7 +466,7 @@ public virtual void Serialize(CeresNode node) serializedData = JsonUtility.ToJson(node); #if UNITY_EDITOR uobjectLinks = Array.Empty(); - if(!Application.isPlaying) + if (!Application.isPlaying) { var obj = JObject.Parse(serializedData); /* Persistent instanceID */ @@ -477,7 +475,7 @@ public virtual void Serialize(CeresNode node) if (prop.Name != "instanceID") continue; var instanceId = (int)prop.Value; var uObject = UnityEditor.EditorUtility.InstanceIDToObject(instanceId); - if(uObject) + if (uObject) { ArrayUtils.Add(ref uobjectLinks, new UObjectLink(uObject)); } @@ -505,7 +503,7 @@ public CeresNode Deserialize(Type outNodeType) { var linkedUObject = uObject.linkedObject; prop.Value = linkedUObject == null ? 0 : linkedUObject.GetInstanceID(); - if(linkedUObject && LogUObjectRelink) + if (linkedUObject && CeresAPI.LogUObjectRelink) { CeresAPI.Log($"Relink UObject {instanceId} to {uObject.linkedObject.name} {prop.Value}"); } diff --git a/Runtime/Core/Models/Graph/Nodes/InvalidNode.cs b/Runtime/Core/Models/Graph/Nodes/InvalidNode.cs index fb7417f..e4fb05b 100644 --- a/Runtime/Core/Models/Graph/Nodes/InvalidNode.cs +++ b/Runtime/Core/Models/Graph/Nodes/InvalidNode.cs @@ -1,8 +1,10 @@ +using System; using Ceres.Annotations; using UnityEngine; namespace Ceres.Graph { - [CeresGroup(Annotations.CeresGroup.Hidden)] + [Serializable] + [CeresGroup(CeresGroup.Hidden)] [CeresLabel(NodeLabel)] [NodeInfo(NodeInfo)] internal sealed class InvalidNode : CeresNode diff --git a/Runtime/Core/Models/Graph/Ports/CeresPort.cs b/Runtime/Core/Models/Graph/Ports/CeresPort.cs index 356327d..1d5a146 100644 --- a/Runtime/Core/Models/Graph/Ports/CeresPort.cs +++ b/Runtime/Core/Models/Graph/Ports/CeresPort.cs @@ -7,7 +7,6 @@ using Chris; using Chris.Serialization; using Unity.Collections.LowLevel.Unsafe; -using UnityEngine; namespace Ceres.Graph { public interface IPort @@ -23,7 +22,7 @@ public interface IPort: IPort /// /// Base class for ceres graph port /// - public abstract class CeresPort: IPort + public abstract class CeresPort: IPort, IDisposable { protected class PortCompatibleStructure { @@ -122,13 +121,10 @@ public virtual void AssignValueGetter(IPort port) { AdaptedGetter = port; } - - static CeresPort() + + public virtual void Dispose() { - /* Implicit conversation */ - CeresPort.MakeCompatibleTo(f => (int)f); - CeresPort.MakeCompatibleTo(i => i); - CeresPort.MakeCompatibleTo(vector3 => vector3); + AdaptedGetter = null; } } @@ -269,6 +265,13 @@ public override object GetValue() { return Value; } + + public override void Dispose() + { + _getter = null; + defaultValue = default; + base.Dispose(); + } } public class AdapterPort: IPort diff --git a/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs b/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs new file mode 100644 index 0000000..04dad84 --- /dev/null +++ b/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs @@ -0,0 +1,40 @@ +using Ceres.Graph; +using Chris.Schedulers; +using Unity.Collections.LowLevel.Unsafe; +using UnityEngine; +namespace Chris.Gameplay.Flow.Utilities +{ + internal static class CeresPortSetup + { + [RuntimeInitializeOnLoadMethod] +#if UNITY_EDITOR + [UnityEditor.InitializeOnLoadMethod] +#endif + private static void InitializeOnLoad() + { + /* Implicit conversation */ + // ======================== Value type =========================== // + CeresPort.MakeCompatibleTo(f => (int)f); + CeresPort.MakeCompatibleTo(i => i); + CeresPort.MakeCompatibleTo(vector3 => vector3); + // ======================== Value type =========================== // + // ========================= Scheduler =========================== // + unsafe + { + CeresPort.MakeCompatibleTo(handle => + { + double value = default; + UnsafeUtility.CopyStructureToPtr(ref handle, &value); + return value; + }); + CeresPort.MakeCompatibleTo(d => + { + SchedulerHandle handle = default; + UnsafeUtility.CopyStructureToPtr(ref d, &handle); + return handle; + }); + } + // ========================= Scheduler =========================== // + } + } +} \ No newline at end of file diff --git a/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs.meta b/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs.meta new file mode 100644 index 0000000..6fe1d76 --- /dev/null +++ b/Runtime/Core/Models/Graph/Ports/CeresPortSetup.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa28bf51a2ee4255a53fd050d99fe75e +timeCreated: 1737783495 \ No newline at end of file diff --git a/Runtime/Core/Models/Variables/SharedVariable.cs b/Runtime/Core/Models/Variables/SharedVariable.cs index d3d8e3f..07c8839 100644 --- a/Runtime/Core/Models/Variables/SharedVariable.cs +++ b/Runtime/Core/Models/Variables/SharedVariable.cs @@ -7,7 +7,7 @@ namespace Ceres /// Variable can be shared between behaviors in behavior tree /// [Serializable] - public abstract class SharedVariable : ICloneable + public abstract class SharedVariable : ICloneable, IDisposable { /// /// Whether variable is shared @@ -67,7 +67,7 @@ public string Name /// /// Unbind self /// - public abstract void Unbind(); + public abstract void Dispose(); /// /// Clone shared variable by deep copy, an option here is to override for preventing using reflection @@ -159,7 +159,7 @@ public override void Bind(SharedVariable other) } } - public override void Unbind() + public override void Dispose() { Getter = null; Setter = null; diff --git a/Runtime/Core/Utilities/GraphTypeExtensions.cs b/Runtime/Core/Utilities/GraphTypeExtensions.cs index 88164d7..063d98c 100644 --- a/Runtime/Core/Utilities/GraphTypeExtensions.cs +++ b/Runtime/Core/Utilities/GraphTypeExtensions.cs @@ -108,5 +108,47 @@ public static bool IsIList(this Type type) if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(List<>)) return true; return type.IsArray; } + + public static bool IsIListPort(this Type fieldType) + { + if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) + { + var genericArgument = fieldType.GetGenericArguments()[0]; + if (typeof(CeresPort).IsAssignableFrom(genericArgument)) + { + return true; + } + } + else if (fieldType.IsArray) + { + var elementType = fieldType.GetElementType(); + if (typeof(CeresPort).IsAssignableFrom(elementType)) + { + return true; + } + } + return false; + } + + public static bool IsIListVariable(this Type fieldType) + { + if (fieldType.IsGenericType && fieldType.GetGenericTypeDefinition() == typeof(List<>)) + { + var genericArgument = fieldType.GetGenericArguments()[0]; + if (typeof(SharedVariable).IsAssignableFrom(genericArgument)) + { + return true; + } + } + else if (fieldType.IsArray) + { + var elementType = fieldType.GetElementType(); + if (typeof(SharedVariable).IsAssignableFrom(elementType)) + { + return true; + } + } + return false; + } } } \ No newline at end of file diff --git a/Runtime/Core/Utilities/SubclassSearchUtility.cs b/Runtime/Core/Utilities/SubclassSearchUtility.cs index d33a2a9..4bdbf4d 100644 --- a/Runtime/Core/Utilities/SubclassSearchUtility.cs +++ b/Runtime/Core/Utilities/SubclassSearchUtility.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Reflection; using Ceres.Annotations; - namespace Ceres.Utilities { public static class SubClassSearchUtility diff --git a/Runtime/Flow/Models/ExecutableReflection.cs b/Runtime/Flow/Models/ExecutableReflection.cs index 259da06..faa5381 100644 --- a/Runtime/Flow/Models/ExecutableReflection.cs +++ b/Runtime/Flow/Models/ExecutableReflection.cs @@ -4,6 +4,8 @@ using System.Reflection; using Ceres.Annotations; using Ceres.Graph.Flow.Annotations; +using Ceres.Graph.Flow.Utilities; +using UnityEngine.Assertions; namespace Ceres.Graph.Flow { public enum ExecutableFunctionType @@ -157,7 +159,7 @@ public bool AmbiguousEquals(ExecutableFunctionInfo other) public class ExecutableReflection: ExecutableReflection { - public class ExecutableFunction: Flow.ExecutableFunction + public unsafe class ExecutableFunction: Flow.ExecutableFunction { public readonly ExecutableFunctionInfo FunctionInfo; @@ -167,6 +169,8 @@ public class ExecutableFunction: Flow.ExecutableFunction public readonly ExecutableFunc ExecutableFunc; + public readonly void* FunctionPtr; + internal ExecutableFunction(ExecutableFunctionInfo functionInfo, MethodInfo methodInfo) { FunctionInfo = functionInfo; @@ -174,19 +178,45 @@ internal ExecutableFunction(ExecutableFunctionInfo functionInfo, MethodInfo meth ExecutableAction = new ExecutableAction(MethodInfo); ExecutableFunc = new ExecutableFunc(MethodInfo); } + + internal ExecutableFunction(ExecutableFunctionInfo functionInfo, void* functionPtr) + { + FunctionInfo = functionInfo; + FunctionPtr = functionPtr; + ExecutableAction = new ExecutableAction(FunctionPtr); + ExecutableFunc = new ExecutableFunc(FunctionPtr); + } + + internal ExecutableFunction(ExecutableFunctionInfo functionInfo, MethodInfo methodInfo, void* functionPtr) + { + FunctionInfo = functionInfo; + MethodInfo = methodInfo; + FunctionPtr = functionPtr; + ExecutableAction = new ExecutableAction(FunctionPtr); + ExecutableFunc = new ExecutableFunc(FunctionPtr); + } } private readonly List _functions = new(); - public ExecutableReflection() + private ExecutableReflection() { - typeof(TTarget).GetMethods(BindingFlags.Static | BindingFlags.Public) - .Where(x=>x.GetCustomAttribute() != null) - .ToList() - .ForEach(methodInfo => - { - RegisterExecutableFunction(ExecutableFunctionType.StaticMethod, methodInfo); - }); + _instance = this; + if (typeof(TTarget).IsSubclassOf(typeof(ExecutableFunctionLibrary))) + { +#if UNITY_EDITOR + typeof(TTarget).GetMethods(BindingFlags.Static | BindingFlags.Public) + .Where(x => x.GetCustomAttribute() != null) + .ToList() + .ForEach(methodInfo => + { + RegisterExecutableFunction(ExecutableFunctionType.StaticMethod, methodInfo); + }); +#endif + Activator.CreateInstance(typeof(TTarget)); + return; + } + typeof(TTarget).GetMethods(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic) .Where(x=>x.GetCustomAttribute() != null) .ToList() @@ -222,6 +252,23 @@ private void RegisterExecutableFunction(ExecutableFunctionType functionType, Met var functionStructure = new ExecutableFunction(functionInfo, methodInfo); _functions.Add(functionStructure); } + + internal static unsafe void RegisterStaticExecutableFunction(string functionName, int parameterCount, void* functionPtr) + { + var functionInfo = new ExecutableFunctionInfo(ExecutableFunctionType.StaticMethod, functionName, parameterCount); +#if UNITY_EDITOR + var function = Instance.FindFunction_Internal(functionInfo); + if (function != null) + { + Instance._functions.Remove(function); + var overrideStructure = new ExecutableFunction(functionInfo, function.MethodInfo, functionPtr); + Instance._functions.Add(overrideStructure); + return; + } +#endif + var functionStructure = new ExecutableFunction(functionInfo, functionPtr); + Instance._functions.Add(functionStructure); + } private ExecutableFunction FindFunction_Internal(ExecutableFunctionInfo functionInfo) { @@ -260,7 +307,7 @@ private ExecutableFunction GetFunction_Internal(ExecutableFunctionInfo functionI var functionType = functionInfo.FunctionType; var functionName = functionInfo.FunctionName; - MethodInfo methodInfo = functionType switch + var methodInfo = functionType switch { ExecutableFunctionType.PropertySetter => typeof(TTarget).GetProperty(functionName, BindingFlags.Public | BindingFlags.Instance)!.SetMethod, @@ -277,29 +324,34 @@ private ExecutableFunction GetFunction_Internal(ExecutableFunctionInfo functionI } - public abstract class ExecutableDelegate + public abstract unsafe class ExecutableDelegate { protected Delegate Delegate; - protected IntPtr FunctionPtr; + protected readonly void* FunctionPtr; - protected readonly bool IsStatic; + public readonly bool IsStatic; protected readonly MethodInfo MethodInfo; protected ExecutableDelegate(MethodInfo methodInfo) { +#if !UNITY_EDITOR + Assert.IsFalse(methodInfo.IsStatic); +#endif MethodInfo = methodInfo; - IsStatic = methodInfo.IsStatic; - if (IsStatic) - { - FunctionPtr = methodInfo.MethodHandle.GetFunctionPointer(); - } + IsStatic = false; + } + + protected ExecutableDelegate(void* functionPtr) + { + IsStatic = true; + FunctionPtr = functionPtr; } protected static void ReallocateDelegateIfNeed(ref Delegate outDelegate, MethodInfo methodInfo) where TDelegate: Delegate { - if (methodInfo.IsStatic) + if (methodInfo == null || methodInfo.IsStatic) { return; } @@ -317,12 +369,16 @@ protected static void ReallocateDelegateIfNeed(ref Delegate outDelega } } - public class ExecutableAction: ExecutableDelegate + public unsafe class ExecutableAction: ExecutableDelegate { internal ExecutableAction(MethodInfo methodInfo) : base(methodInfo) { } + internal ExecutableAction(void* functionPtr) : base(functionPtr) + { + } + private void ReallocateDelegateIfNeed() { ReallocateDelegateIfNeed>(ref Delegate, MethodInfo); @@ -358,7 +414,7 @@ private void ReallocateDelegateIfNeed() ReallocateDelegateIfNeed>(ref Delegate, MethodInfo); } - public unsafe void Invoke(TTarget target) + public void Invoke(TTarget target) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -366,10 +422,11 @@ public unsafe void Invoke(TTarget target) ((delegate* )FunctionPtr)(); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target); } - public unsafe void Invoke(TTarget target, T1 arg1) + public void Invoke(TTarget target, T1 arg1) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -377,10 +434,11 @@ public unsafe void Invoke(TTarget target, T1 arg1) ((delegate* )FunctionPtr)(arg1); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1); } - public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2) + public void Invoke(TTarget target, T1 arg1, T2 arg2) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -388,10 +446,11 @@ public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2) ((delegate* )FunctionPtr)(arg1, arg2); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1, arg2); } - public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3) + public void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -399,10 +458,11 @@ public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3) ((delegate* )FunctionPtr)(arg1, arg2, arg3); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1, arg2, arg3); } - public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + public void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -410,10 +470,11 @@ public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 a ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1, arg2, arg3, arg4); } - public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + public void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -421,10 +482,11 @@ public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4, arg5); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1, arg2, arg3, arg4, arg5); } - public unsafe void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) + public void Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { ReallocateDelegateIfNeed(); if (IsStatic) @@ -432,16 +494,21 @@ public unsafe void Invoke(TTarget target, T1 arg1, T2 ar ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4, arg5, arg6); return; } + Assert.IsNotNull(Delegate); ((Action)Delegate).Invoke(target, arg1, arg2, arg3, arg4, arg5, arg6); } } - public class ExecutableFunc: ExecutableDelegate + public unsafe class ExecutableFunc: ExecutableDelegate { internal ExecutableFunc(MethodInfo methodInfo) : base(methodInfo) { } + internal ExecutableFunc(void* functionPtr) : base(functionPtr) + { + } + private void ReallocateDelegateIfNeed() { ReallocateDelegateIfNeed>(ref Delegate, MethodInfo); @@ -477,73 +544,80 @@ private void ReallocateDelegateIfNeed() ReallocateDelegateIfNeed>(ref Delegate, MethodInfo); } - public unsafe TR Invoke(TTarget target) + public TR Invoke(TTarget target) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target); } - public unsafe TR Invoke(TTarget target, T1 arg1) + public TR Invoke(TTarget target, T1 arg1) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1); } - public unsafe TR Invoke(TTarget target, T1 arg1, T2 arg2) + public TR Invoke(TTarget target, T1 arg1, T2 arg2) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1, arg2); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1, arg2); } - public unsafe TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3) + public TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1, arg2, arg3); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1, arg2, arg3); } - public unsafe TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4) + public TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1, arg2, arg3, arg4); } - public unsafe TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) + public TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4, arg5); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1, arg2, arg3, arg4, arg5); } - public unsafe TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) + public TR Invoke(TTarget target, T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, T6 arg6) { ReallocateDelegateIfNeed(); if (IsStatic) { return ((delegate* )FunctionPtr)(arg1, arg2, arg3, arg4, arg5, arg6); } + Assert.IsNotNull(Delegate); return ((Func)Delegate).Invoke(target, arg1, arg2, arg3, arg4, arg5, arg6); } } diff --git a/Runtime/Flow/Models/Nodes/Utilities/FlowNode_ExecuteFunction.cs b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_ExecuteFunction.cs index abedca1..476638c 100644 --- a/Runtime/Flow/Models/Nodes/Utilities/FlowNode_ExecuteFunction.cs +++ b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_ExecuteFunction.cs @@ -2,6 +2,7 @@ using System.Reflection; using Ceres.Annotations; using UnityEngine; +using UnityEngine.Assertions; using UObject = UnityEngine.Object; namespace Ceres.Graph.Flow.Utilities { @@ -124,6 +125,7 @@ public void OnAfterDeserialize() try { Delegate = GetExecutableFunction().ExecutableFunc; + Assert.IsTrue(Delegate.IsStatic == isStatic); } catch(ArgumentException) { @@ -156,6 +158,7 @@ public void OnAfterDeserialize() try { Delegate = GetExecutableFunction().ExecutableAction; + Assert.IsTrue(Delegate.IsStatic == isStatic); } catch(ArgumentException) { diff --git a/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs new file mode 100644 index 0000000..9b38247 --- /dev/null +++ b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs @@ -0,0 +1,42 @@ +using System; +using Ceres.Annotations; +using R3.Chris; +using Chris.Resource; +using UObject = UnityEngine.Object; +namespace Ceres.Graph.Flow.Utilities +{ + [Serializable] + [CeresGroup("Utilities")] + [CeresLabel("Load {0} Async")] + public sealed class FlowNode_SoftAssetReferenceTLoadAssetAsync: FlowNode where TObject: UObject + { + [InputPort, HideInGraphEditor] + public CeresPort> reference; + + [InputPort] + public DelegatePort> onComplete; + + protected override void LocalExecute(ExecutionContext executionContext) + { + reference.Value.LoadAsync().AddTo(executionContext.Graph).RegisterCallback(onComplete.Value); + } + } + + [Serializable] + [CeresGroup("Utilities")] + [CeresLabel("Load Asset Async")] + [RequirePort(typeof(SoftAssetReference))] + public sealed class FlowNode_SoftAssetReferenceLoadAssetAsync: FlowNode + { + [InputPort, HideInGraphEditor] + public CeresPort reference; + + [InputPort] + public DelegatePort> onComplete; + + protected override void LocalExecute(ExecutionContext executionContext) + { + reference.Value.LoadAsync().AddTo(executionContext.Graph).RegisterCallback(onComplete.Value); + } + } +} \ No newline at end of file diff --git a/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs.meta b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs.meta new file mode 100644 index 0000000..c4090f3 --- /dev/null +++ b/Runtime/Flow/Models/Nodes/Utilities/FlowNode_SoftAssetReferenceTLoadAssetAsync.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1317c567fc724c88a31d94db37e0c346 +timeCreated: 1736668939 \ No newline at end of file diff --git a/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs b/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs new file mode 100644 index 0000000..b59fe83 --- /dev/null +++ b/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs @@ -0,0 +1,36 @@ +using System; +using Chris; +using Chris.Resource; +namespace Ceres.Graph.Flow.Utilities +{ + public class FlowNode_SoftAssetReferenceTLoadAssetAsync_Template: GenericNodeTemplate + { + public override bool RequirePort() + { + return true; + } + + public override bool CanFilterPort(Type portValueType) + { + if (portValueType == null) return false; + if (!portValueType.IsGenericType) return false; + return portValueType.GetGenericTypeDefinition() == typeof(SoftAssetReference<>); + } + + public override Type[] GetGenericArguments(Type portValueType, Type selectArgumentType) + { + return new[] { selectArgumentType }; + } + + public override Type[] GetAvailableArgumentTypes(Type portValueType) + { + return new[] { ReflectionUtility.GetGenericArgumentType(portValueType)}; + } + + protected override string GetTargetName(Type[] argumentTypes) + { + var genericType = typeof(SoftAssetReference<>).MakeGenericType(argumentTypes[0]); + return CeresNode.GetTargetSubtitle(genericType); + } + } +} \ No newline at end of file diff --git a/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs.meta b/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs.meta new file mode 100644 index 0000000..766cc4c --- /dev/null +++ b/Runtime/Flow/Models/Nodes/Utilities/Templates/FlowNode_SoftAssetReferenceTLoadAssetAsync_Template.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fd0f8c06799d48da9454b52a4ac3a5e6 +timeCreated: 1736670082 \ No newline at end of file diff --git a/Runtime/Flow/Utilities/ExecutableFunctionRegistry.cs b/Runtime/Flow/Utilities/ExecutableFunctionRegistry.cs index 00a554c..6ee0f5a 100644 --- a/Runtime/Flow/Utilities/ExecutableFunctionRegistry.cs +++ b/Runtime/Flow/Utilities/ExecutableFunctionRegistry.cs @@ -9,11 +9,39 @@ namespace Ceres.Graph.Flow.Utilities /// /// Derived from this class to add custom static functions /// + /// Must add partial modifier public abstract class ExecutableFunctionLibrary { + /// + /// Collect all static executable functions in this library + /// + protected virtual void CollectExecutableFunctions() + { + + } + protected ExecutableFunctionLibrary() + { + CollectExecutableFunctions(); + } + + /// + /// Register static executable function to reflection system + /// + /// + /// + /// + /// + protected static unsafe void RegisterExecutableFunctions(string functionName, int parameterCount, void* functionPtr) + where TLibrary: ExecutableFunctionLibrary + { + ExecutableReflection.RegisterStaticExecutableFunction(functionName, parameterCount, functionPtr); + } } + /// + /// Helper class for query executable functions + /// public class ExecutableFunctionRegistry { private readonly Dictionary _retargetFunctionTables; diff --git a/Runtime/Flow/Utilities/Libraries/CeresExecutableLibrary.cs b/Runtime/Flow/Utilities/Libraries/CeresExecutableLibrary.cs index ad92888..292de54 100644 --- a/Runtime/Flow/Utilities/Libraries/CeresExecutableLibrary.cs +++ b/Runtime/Flow/Utilities/Libraries/CeresExecutableLibrary.cs @@ -1,6 +1,5 @@ using Ceres.Annotations; using Ceres.Graph.Flow.Annotations; -using Chris.Serialization; using UnityEngine; using UnityEngine.Scripting; namespace Ceres.Graph.Flow.Utilities @@ -9,9 +8,8 @@ namespace Ceres.Graph.Flow.Utilities /// Executable function library for ceres /// [Preserve] - [FormerlySerializedType("Ceres.Graph.Flow.Utilities.CeresExecutableFunctionLibrary, Ceres")] [CeresGroup("Ceres")] - public class CeresExecutableLibrary: ExecutableFunctionLibrary + public partial class CeresExecutableLibrary: ExecutableFunctionLibrary { [ExecutableFunction, CeresLabel("Set LogLevel")] public static void Flow_SetLogLevel(LogType logType) diff --git a/Runtime/Flow/Utilities/Libraries/MathExecutableLibrary.cs b/Runtime/Flow/Utilities/Libraries/MathExecutableLibrary.cs index 25a1033..9675910 100644 --- a/Runtime/Flow/Utilities/Libraries/MathExecutableLibrary.cs +++ b/Runtime/Flow/Utilities/Libraries/MathExecutableLibrary.cs @@ -1,6 +1,5 @@ using Ceres.Annotations; using Ceres.Graph.Flow.Annotations; -using Chris.Serialization; using UnityEngine.Scripting; using UnityEngine; namespace Ceres.Graph.Flow.Utilities @@ -9,8 +8,7 @@ namespace Ceres.Graph.Flow.Utilities /// Executable function library for basic math operations /// [Preserve] - [FormerlySerializedType("Ceres.Graph.Flow.Utilities.MathExecutableFunctionLibrary, Ceres")] - public class MathExecutableLibrary : ExecutableFunctionLibrary + public partial class MathExecutableLibrary : ExecutableFunctionLibrary { #region Float diff --git a/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs b/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs new file mode 100644 index 0000000..ba52cfc --- /dev/null +++ b/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs @@ -0,0 +1,42 @@ +using Ceres.Annotations; +using Ceres.Graph.Flow.Annotations; +using Chris.Events; +using Chris.Resource; +using UnityEngine; +using UnityEngine.Scripting; +using UObject = UnityEngine.Object; +using R3; +namespace Ceres.Graph.Flow.Utilities +{ + /// + /// Executable function library for Chris.Resource + /// + [Preserve] + [CeresGroup("Resource")] + public partial class ResourceExecutableLibrary: ExecutableFunctionLibrary + { + [ExecutableFunction, CeresLabel("Load Asset Synchronous")] + public static UObject Flow_LoadAssetSynchronous(string address) + { + return ResourceSystem.LoadAssetAsync(address).AddTo(EventSystem.Instance).WaitForCompletion(); + } + + [ExecutableFunction, CeresLabel("Load Asset Async")] + public static void Flow_LoadAssetAsync(string address, EventDelegate onComplete) + { + ResourceSystem.LoadAssetAsync(address, onComplete).AddTo(EventSystem.Instance); + } + + [ExecutableFunction, CeresLabel("Instantiate Synchronous")] + public static GameObject Flow_InstantiateAsync(string address, Transform parent) + { + return ResourceSystem.InstantiateAsync(address, parent).AddTo(EventSystem.Instance).WaitForCompletion(); + } + + [ExecutableFunction, CeresLabel("Instantiate Async")] + public static void Flow_InstantiateAsync(string address, Transform parent, EventDelegate onComplete) + { + ResourceSystem.InstantiateAsync(address, parent, onComplete).AddTo(EventSystem.Instance); + } + } +} \ No newline at end of file diff --git a/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs.meta b/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs.meta new file mode 100644 index 0000000..d102699 --- /dev/null +++ b/Runtime/Flow/Utilities/Libraries/ResourceExecutableLibrary..cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: e0ac018726e64098aa3838e7f58847dd +timeCreated: 1736667844 \ No newline at end of file diff --git a/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs b/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs new file mode 100644 index 0000000..5367281 --- /dev/null +++ b/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs @@ -0,0 +1,43 @@ +using Ceres.Annotations; +using Ceres.Graph.Flow.Annotations; +using Chris.Schedulers; +using UnityEngine.Scripting; +namespace Ceres.Graph.Flow.Utilities +{ + /// + /// Executable function library for Chris.Schedulers + /// + [Preserve] + [CeresGroup("Scheduler")] + public partial class SchedulerExecutableLibrary: ExecutableFunctionLibrary + { + #region Scheduler + + [ExecutableFunction, CeresLabel("Schedule Timer by Event")] + public static SchedulerHandle Flow_SchedulerDelay( + float delaySeconds, EventDelegate onComplete, EventDelegate onUpdate, + TickFrame tickFrame, bool isLooped, bool ignoreTimeScale) + { + var handle = Scheduler.Delay(delaySeconds,onComplete,onUpdate, + tickFrame, isLooped, ignoreTimeScale); + return handle; + } + + [ExecutableFunction, CeresLabel("Schedule FrameCounter by Event")] + public static SchedulerHandle Flow_SchedulerWaitFrame( + int frame, EventDelegate onComplete, EventDelegate onUpdate, + TickFrame tickFrame, bool isLooped) + { + var handle = Scheduler.WaitFrame(frame, onComplete, onUpdate, tickFrame, isLooped); + return handle; + } + + [ExecutableFunction, CeresLabel("Cancel Scheduler")] + public static void Flow_SchedulerCancel(SchedulerHandle handle) + { + handle.Cancel(); + } + + #endregion Scheduler + } +} \ No newline at end of file diff --git a/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs.meta b/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs.meta new file mode 100644 index 0000000..f74b061 --- /dev/null +++ b/Runtime/Flow/Utilities/Libraries/SchedulerExecutableLibrary.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 866d675c7a6f457eb0f3b24879a6543c +timeCreated: 1736667765 \ No newline at end of file diff --git a/Runtime/Flow/Utilities/Libraries/UnityExecutableLibrary.cs b/Runtime/Flow/Utilities/Libraries/UnityExecutableLibrary.cs index 6943591..66c6a38 100644 --- a/Runtime/Flow/Utilities/Libraries/UnityExecutableLibrary.cs +++ b/Runtime/Flow/Utilities/Libraries/UnityExecutableLibrary.cs @@ -10,9 +10,8 @@ namespace Ceres.Graph.Flow.Utilities /// Executable function library for Unity built-in types /// [Preserve] - [FormerlySerializedType("Ceres.Graph.Flow.Utilities.UnityExecutableFunctionLibrary, Ceres")] [CeresGroup("Unity")] - public class UnityExecutableLibrary: ExecutableFunctionLibrary + public partial class UnityExecutableLibrary: ExecutableFunctionLibrary { #region UObject diff --git a/Runtime/SourceGenerators/Ceres.SourceGenerator.dll b/Runtime/SourceGenerators/Ceres.SourceGenerator.dll index eab0025..b670a7a 100644 Binary files a/Runtime/SourceGenerators/Ceres.SourceGenerator.dll and b/Runtime/SourceGenerators/Ceres.SourceGenerator.dll differ diff --git a/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibraryGeneratorContext.cs b/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibraryGeneratorContext.cs new file mode 100644 index 0000000..376cdb7 --- /dev/null +++ b/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibraryGeneratorContext.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text; +namespace Ceres.SourceGenerator.Generators +{ + internal class ExecutableLibraryGeneratorContext + { + private static readonly string StartTemplate = +""" +/// +/// This file is auto-generated by Ceres.SourceGenerator. +/// All changes will be discarded. +/// +{USINGNAMESPACE} +namespace {NAMESPACE} +{ + [System.Runtime.CompilerServices.CompilerGenerated] + public partial class {CLASSNAME} + { + protected override unsafe void CollectExecutableFunctions() + { +"""; + private static readonly string EndTemplate = +""" + + } + } +} +"""; + + + public string Namespace; + + public string ClassName; + + public List FunctionInfos; + + public HashSet Namespaces; + + public string GenerateCode() + { + var sb = new StringBuilder(); + var namedCode = StartTemplate + .Replace("{USINGNAMESPACE}", string.Join("\n", Namespaces)) + .Replace("{NAMESPACE}", Namespace) + .Replace("{CLASSNAME}", ClassName); + sb.Append(namedCode); + foreach (var function in FunctionInfos) + { + var list = new List(); + list.AddRange(function.Parameters.Select(x => x.ParameterType)); + list.Add(function.ReturnParameter.ParameterType); + string delegateStructure = string.Join(", ", list.ToArray()); + sb.Append( +$""" + + RegisterExecutableFunctions<{ClassName}>(nameof({function.MethodName}), {function.Parameters.Count}, (delegate* <{delegateStructure}>)&{function.MethodName}); +"""); + } + sb.Append(EndTemplate); + return sb.ToString(); + } + } +} diff --git a/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibrarySourceGenerator.cs b/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibrarySourceGenerator.cs new file mode 100644 index 0000000..a49a7ac --- /dev/null +++ b/Runtime/SourceGenerators/Source~/Ceres.SourceGenerator/Generators/ExecutableLibrarySourceGenerator.cs @@ -0,0 +1,217 @@ +using Ceres.SourceGenerator.Generators; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection.Metadata; + +namespace Ceres.SourceGenerator +{ + [Generator] + public class ExecutableLibrarySourceGenerator : ISourceGenerator + { + public void Initialize(GeneratorInitializationContext context) + { + context.RegisterForSyntaxNotifications(() => new ExecutableLibrarySyntaxReceiver()); + } + + private static bool ShouldRunGenerator(GeneratorExecutionContext executionContext) + { + // Skip running if no references to ceres are passed to the compilation + return executionContext.Compilation.Assembly.Name.StartsWith("Ceres", StringComparison.Ordinal) || + executionContext.Compilation.ReferencedAssemblyNames.Any(r => r.Name.Equals("Ceres", StringComparison.Ordinal)); + } + + public struct GeneratedFile + { + public string ClassName; + + public string Namespace; + + public string GeneratedFileName; + + public string Code; + } + + public void Execute(GeneratorExecutionContext context) + { + var receiver = context.SyntaxReceiver as ExecutableLibrarySyntaxReceiver; + if (receiver == null) return; + + if (!ShouldRunGenerator(context)) + return; + + Helpers.SetupContext(context); + Debug.LogInfo($"Execute assmebly {context.Compilation.Assembly.Name}"); + + //If the attach_debugger key is present (but without value) the returned string is the empty string (not null) + var debugAssembly = context.GetOptionsString(GlobalOptions.AttachDebugger); + if (debugAssembly != null) + { + Debug.LaunchDebugger(context, debugAssembly); + } + + List generatedFiles = []; + + foreach (var classDeclaration in receiver.Candidates) + { + var className = classDeclaration.Identifier.Text; + Debug.LogInfo($"Analyze {className}"); + + var semanticModel = context.Compilation.GetSemanticModel(classDeclaration.SyntaxTree); + var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration); + + if (classSymbol == null) + { + continue; + } + + var namespaceNode = classDeclaration.Parent as NamespaceDeclarationSyntax; + var namespaceName = namespaceNode.Name.ToString(); + + ExecutableLibraryGeneratorContext generatorContext = new() + { + Namespace = namespaceName, + ClassName = className, + FunctionInfos = receiver.Methods[classDeclaration], + Namespaces = receiver.Namespaces[classDeclaration] + }; + var generatedCode = generatorContext.GenerateCode(); + generatedFiles.Add(new GeneratedFile + { + ClassName = className, + Namespace = namespaceName, + Code = generatedCode, + GeneratedFileName = $"{className}.gen.cs" + }); + } + + // Always delete all the previously generated files + if (Helpers.CanWriteFiles) + { + var outputFolder = Path.Combine(Helpers.GetOutputPath(), $"{context.Compilation.AssemblyName}"); + if (Directory.Exists(outputFolder)) + Directory.Delete(outputFolder, true); + if (generatedFiles.Count != 0) + Directory.CreateDirectory(outputFolder); + } + + foreach (var nameAndSource in generatedFiles) + { + Debug.LogInfo($"Generate {nameAndSource.GeneratedFileName}"); + var sourceText = SourceText.From(nameAndSource.Code, System.Text.Encoding.UTF8); + // Normalize filename for hint purpose. Special characters are not supported anymore + // var hintName = uniqueName.Replace('/', '_').Replace('+', '-'); + // TODO: compute a normalized hash of that name using a common stable hash algorithm + var sourcePath = Path.Combine($"{context.Compilation.AssemblyName}", nameAndSource.GeneratedFileName); + var hintName = TypeHash.FNV1A64(sourcePath).ToString(); + context.AddSource(hintName, sourceText.WithInitialLineDirective(sourcePath)); + try + { + if (Helpers.CanWriteFiles) + File.WriteAllText(Path.Combine(Helpers.GetOutputPath(), sourcePath), nameAndSource.Code); + } + catch (Exception e) + { + // In the rare event/occasion when this happen, at the very least don't bother the user and move forward + Debug.LogWarning($"cannot write file {sourcePath}. An exception has been thrown:{e}"); + } + } + } + } + + public class ExecutableFunctionParameterInfo + { + public string ParameterType; + + public string ParameterName; + } + + public class ExecutableFunctionInfo + { + public string MethodName; + + public readonly List Parameters = new(); + + public ExecutableFunctionParameterInfo ReturnParameter; + } + + public class ExecutableLibrarySyntaxReceiver : ISyntaxReceiver + { + public readonly List Candidates = []; + + public readonly Dictionary> Methods = new(); + + public readonly Dictionary> Namespaces = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) + { + if (syntaxNode is not ClassDeclarationSyntax classNode) + return; + + if (classNode.BaseList == null || classNode.BaseList.Types.Count == 0) + return; + + if (!classNode.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword))) + { + return; + } + + // Check inherit from ExecutableFunctionLibrary + if (!classNode.BaseList.Types.Any(baseType => + baseType.Type is IdentifierNameSyntax identifierName && + identifierName.Identifier.Text == "ExecutableFunctionLibrary")) + { + return; + } + + Candidates.Add(classNode); + + var namespaces = new HashSet(); + var root = classNode.SyntaxTree.GetRoot(); + var usings = root.DescendantNodes().OfType(); + foreach (var usingDirective in usings) + { + var namespaceName = usingDirective.ToString(); + namespaces.Add(namespaceName); + } + Namespaces[classNode] = namespaces; + + var methodInfos = new List(); + foreach (var member in classNode.Members) + { + if (member is MethodDeclarationSyntax methodNode) + { + var methodInfo = new ExecutableFunctionInfo + { + MethodName = methodNode.Identifier.Text + }; + + foreach (var parameter in methodNode.ParameterList.Parameters) + { + var parameterInfo = new ExecutableFunctionParameterInfo + { + ParameterType = parameter.Type.ToString(), + ParameterName = parameter.Identifier.Text + }; + methodInfo.Parameters.Add(parameterInfo); + } + + var returnParameterInfo = new ExecutableFunctionParameterInfo + { + ParameterType = methodNode.ReturnType.ToString() + }; + + methodInfo.ReturnParameter = returnParameterInfo; + methodInfos.Add(methodInfo); + } + } + + Methods[classNode] = methodInfos; + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 12b4584..d5a3ad7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.kurisu.ceres", "displayName": "Ceres", - "version": "0.1.3", + "version": "0.1.4", "unity": "2022.3", "description": "Powerful visual scripting toolkit for Unity.", "keywords": [