using GlitchyEngine.Core; using GlitchyEngine.Editor; using GlitchyEngine.Extensions; using GlitchyEngine.Serialization; using ImGuiNET; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; using System.Runtime.Loader; using GlitchyEngine.Native; using GlitchyEngine.Physics; namespace GlitchyEngine; /// /// Contains functions pointer to engine functions that can be called form scripts. /// [StructLayout(LayoutKind.Sequential)] internal unsafe partial struct EngineFunctions { } /// /// Provides the interface between engine and scripts. /// internal static unsafe partial class ScriptGlue { #region Script Glueing infrastructure static ScriptGlue() { ConfigureDllImportResolver(); } private static void ConfigureDllImportResolver() { NativeLibrary.SetDllImportResolver(typeof(ScriptGlue).Assembly, ImportResolver); // TODO: With this we can actually use the official ImGui.NET-Branch in the future! NativeLibrary.SetDllImportResolver(typeof(ImGui).Assembly, ImportResolver); } /// /// Resolves an import request for the DLL __Internal ([DllImport("__Internal")]) to the current executable file. /// This allows calling functions that are part of the native executable (e.g. native libraries we want to use from C# scripts). /// private static IntPtr ImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath) { if (libraryName == "__Internal") { string? mainModuleFileName = Process.GetCurrentProcess().MainModule?.FileName; if (mainModuleFileName != null) return NativeLibrary.Load(mainModuleFileName); throw new DllNotFoundException("Failed to resolve import to current executable: Could not find file name of main module."); } return IntPtr.Zero; } private static EngineFunctions _engineFunctions; private static AssemblyLoadContext? _scriptAssemblyContext; private static Assembly? _appAssembly; /// /// Entity script instances /// private static readonly Dictionary EntityScriptInstances = new(); /// /// Called by the engine to provide a struct containing all functions pointers that can be called from C## /// /// The struct containing the function pointers to the engine functions. [UnmanagedCallersOnly] public static void SetEngineFunctions(EngineFunctions* engineFunctions) { try { _engineFunctions = *engineFunctions; } catch (Exception e) { Log.Exception(e); } } /// /// Called by the engine to load the provided assembly (containing user scripts) and optionally debug symbols. /// [UnmanagedCallersOnly] public static void LoadScriptAssembly(byte* assemblyData, long assemblyLength, byte* pdbData, long pdbLength) { try { using UnmanagedMemoryStream assemblyStream = new(assemblyData, assemblyLength); using UnmanagedMemoryStream? pdbStream = (pdbData != null) ? new UnmanagedMemoryStream(pdbData, pdbLength) : null; LoadAssembly(assemblyStream, pdbStream); } catch (Exception e) { Log.Exception(e); } } private static void LoadAssembly(Stream assemblyStream, Stream? pdbStream) { _scriptAssemblyContext ??= new AssemblyLoadContext("ScriptContext", true); _appAssembly = _scriptAssemblyContext.LoadFromStream(assemblyStream, pdbStream); Debug.Assert(_appAssembly != null); } /// /// Called by the engine to unload the assembly (containing user scripts). /// [UnmanagedCallersOnly] public static void UnloadAssemblies() { try { _scriptAssemblyContext?.Unload(); _scriptAssemblyContext = null; } catch (Exception e) { Log.Exception(e); } } private static NativeScriptClassInfo[]? _unsafeClasses; public struct NativeScriptClassInfo { public IntPtr Name; public Guid Guid; public ScriptMethods Methods; public bool RunInEditMode; } [Flags] public enum ScriptMethods { None = 0, OnCreate = 0x1, OnUpdate = 0x2, OnDestroy = 0x4, OnCollisionEnter2D = 0x8 } /// /// Called by the engine to receive a list of all script classes. /// /// Pointer to the array of s /// The number of elements in /// /// The array returned by must be freed using . /// [UnmanagedCallersOnly] public static void GetScriptClasses(NativeScriptClassInfo** outBuffer, long* length) { try { using var contextualReflection = AssemblyLoadContext.EnterContextualReflection(_appAssembly); Debug.Assert(_appAssembly != null); Internal_FreeScriptClassNames(); var types = _appAssembly.GetTypes(); List<(string Name, Guid Guid, ScriptMethods AvailableMethods, bool runInEditMode)> scriptClasses = new(); foreach (var type in types) { if (type.IsSubclassOf(typeof(Entity))) { string? name = type.FullName; if (name == null) continue; Guid guid = type.GUID; ScriptMethods methods = ScriptMethods.None; static ScriptMethods HasMethod(Type type, string methodName, ScriptMethods methodFlag) { MethodInfo? methodInfo = null; Type? currentType = type; while (methodInfo == null && currentType != typeof(Entity) && currentType != null) { methodInfo = currentType.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); currentType = currentType.BaseType; } return methodInfo != null ? methodFlag : ScriptMethods.None; } methods |= HasMethod(type, nameof(Entity.OnCreate), ScriptMethods.OnCreate); methods |= HasMethod(type, nameof(Entity.OnUpdate), ScriptMethods.OnUpdate); methods |= HasMethod(type, nameof(Entity.OnDestroy), ScriptMethods.OnDestroy); methods |= HasMethod(type, nameof(Entity.OnCollisionEnter2D), ScriptMethods.OnCollisionEnter2D); bool runInEditMode = type.HasCustomAttribute(); scriptClasses.Add((name, guid, methods, runInEditMode)); } } _unsafeClasses = new NativeScriptClassInfo[scriptClasses.Count]; for (int i = 0; i < _unsafeClasses.Length; i++) { _unsafeClasses[i] = new NativeScriptClassInfo() { Guid = scriptClasses[i].Guid, Name = Marshal.StringToCoTaskMemUTF8(scriptClasses[i].Name), Methods = scriptClasses[i].AvailableMethods, RunInEditMode = scriptClasses[i].runInEditMode }; } *outBuffer = (NativeScriptClassInfo*)Marshal.UnsafeAddrOfPinnedArrayElement(_unsafeClasses, 0); *length = _unsafeClasses.Length; } catch (Exception e) { Log.Exception(e); } } /// /// Called by the engine to free the data allocated by /// [UnmanagedCallersOnly] public static void FreeScriptClassNames() { try { Internal_FreeScriptClassNames(); } catch (Exception e) { Log.Exception(e); } } private static void Internal_FreeScriptClassNames() { if (_unsafeClasses == null) return; foreach (NativeScriptClassInfo info in _unsafeClasses) { Marshal.FreeCoTaskMem(info.Name); } _unsafeClasses = null; } [UnmanagedCallersOnly] public static void ShowEntityEditor(UUID entityId) { try { (Entity entity, Type type) = EntityScriptInstances[entityId]; EntityEditor.ShowEntityEditor(entity); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void InvokeEntityOnCreate(UUID entityId) { try { (Entity entity, Type type) = EntityScriptInstances[entityId]; entity.OnCreate(); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static void InvokeEntityOnUpdate(UUID entityId, float deltaTime) { try { (Entity entity, Type type) = EntityScriptInstances[entityId]; entity.OnUpdate(deltaTime); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] public static void InvokeEntityOnCollisionEnter2D(UUID entityId, Collision2D collision) { try { (Entity entity, Type type) = EntityScriptInstances[entityId]; entity.OnCollisionEnter2D(collision); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void InvokeEntityOnDestroy(UUID entityId, byte callDestroy) { try { if (EntityScriptInstances.Remove(entityId, out var match) && callDestroy > 0) { match.Entity.OnDestroy(); } } catch (Exception e) { Log.Exception(e); } } internal static Type? GetTypeFromNativeString(byte* fullTypeName) { if (fullTypeName == null) return null; using var _ = AssemblyLoadContext.EnterContextualReflection(_appAssembly); Debug.Assert(_appAssembly != null); string? typeName = Marshal.PtrToStringUTF8((IntPtr)fullTypeName); Debug.Assert(typeName != null); return _appAssembly.GetType(typeName); } [UnmanagedCallersOnly] public static void CreateScriptInstance(UUID entityId, byte* scriptClassName) { try { Type? scriptType = GetTypeFromNativeString(scriptClassName); Debug.Assert(scriptType != null, "Script class Type not found."); Entity? scriptInstance = ActivatorExtension.CreateEngineObject(scriptType, entityId) as Entity; Debug.Assert(scriptInstance != null, "Failed to create script instance."); EntityScriptInstances.Add(entityId, (scriptInstance, scriptType)); } catch (Exception e) { Log.Exception(e); } } struct ComponentFunctionPointers { public delegate* unmanaged[Cdecl] AddComponent; public delegate* unmanaged[Cdecl] HasComponent; public delegate* unmanaged[Cdecl] RemoveComponent; } private static readonly Dictionary ComponentTypeFunctions = new(); [UnmanagedCallersOnly] public static void RegisterComponentType(byte* fullComponentTypeName, delegate* unmanaged[Cdecl] addComponent, delegate* unmanaged[Cdecl] hasComponent, delegate* unmanaged[Cdecl] removeComponent) { try { string? beefComponentTypeName = Marshal.PtrToStringUTF8((IntPtr)fullComponentTypeName); if (beefComponentTypeName == null) return; foreach(Type componentType in TypeExtension.FindDerivedTypes(typeof(Component))) { if (!componentType.TryGetCustomAttribute(out EngineClassAttribute mapping) || mapping.EngineClassName != beefComponentTypeName) continue; ComponentTypeFunctions[componentType] = new ComponentFunctionPointers { AddComponent = addComponent, HasComponent = hasComponent, RemoveComponent = removeComponent }; return; } Log.Error($"Failed to register component type \"{beefComponentTypeName}\": No matching component class found."); } catch (Exception e) { Log.Exception(e); } } #region EntitySerializer [UnmanagedCallersOnly] public static void CreateSerializationContext(IntPtr engineSerializer) { try { EntitySerializer.CreateSerializationContext(engineSerializer); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void DestroySerializationContext(IntPtr engineSerializer) { try { EntitySerializer.DestroySerializationContext(engineSerializer); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void EntitySerializer_Serialize(UUID entityId, IntPtr engineObject, IntPtr engineSerializer) { try { if (!EntityScriptInstances.TryGetValue(entityId, out var match)) return; EntitySerializer.Serialize(match.Entity, engineObject, engineSerializer); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void EntitySerializer_Deserialize(UUID entityId, IntPtr engineObject, IntPtr engineSerializer) { try { if (!EntityScriptInstances.TryGetValue(entityId, out var match)) return; EntitySerializer.Deserialize(match.Entity, engineObject, engineSerializer); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void EntitySerializer_SerializeStaticFields(byte* fullTypeName, IntPtr engineObject, IntPtr engineSerializer) { try { Type? type = GetTypeFromNativeString(fullTypeName); Debug.Assert(type != null); EntitySerializer.SerializeStaticFields(type, engineObject, engineSerializer); } catch (Exception e) { Log.Exception(e); } } [UnmanagedCallersOnly] public static void EntitySerializer_DeserializeStaticFields(byte* fullTypeName, IntPtr engineObject, IntPtr engineSerializer) { try { Type? type = GetTypeFromNativeString(fullTypeName); Debug.Assert(type != null); EntitySerializer.DeserializeStaticFields(type, engineObject, engineSerializer); } catch (Exception e) { Log.Exception(e); } } #endregion EntitySerializer #endregion #region Custom engine call implementations public static void Entity_AddComponent(UUID entityId, Type componentType) { ComponentTypeFunctions[componentType].AddComponent(entityId); } public static bool Entity_HasComponent(UUID entityId, Type componentType) { EngineResult returnValue = ComponentTypeFunctions[componentType].HasComponent(entityId); EngineErrors.ThrowIfError(returnValue); return returnValue == EngineResult.Ok; } public static void Entity_RemoveComponent(UUID entityId, Type componentType) { ComponentTypeFunctions[componentType].RemoveComponent(entityId); } public static void Entity_GetScriptInstance(UUID entityId, out Entity? instance) { // We currently can implement this method here, because we only have C# scripts. // If we ever need to do something to interop with other script languages, then this would change. if (EntityScriptInstances.TryGetValue(entityId, out var match)) { instance = match.Entity; } instance = null; } public static void Serialization_SerializeField(IntPtr serializationContext, SerializationType type, string fieldName, object? valueObject, string fullTypeName) { StringView fieldNameConverted = StringView.FromManagedString(fieldName); StringView fullTypeNameConverted = StringView.FromManagedString(fullTypeName); void* valueObjectConverted = null; bool deleteValueObject = false; switch (type) { case SerializationType.String: case SerializationType.Enum: if (valueObject is string stringValue) { StringView nativeString = StringView.FromManagedString(stringValue); valueObjectConverted = nativeString.Utf8Ptr; deleteValueObject = true; } break; default: if (valueObject is not null) { #pragma warning disable CS8500 // This takes the address of, gets the size of, or declares a pointer to a managed type object?* objectRef = &valueObject; // Skip Object Header (IntPtr) + Method Table (IntPtr) valueObjectConverted = (byte*)*(IntPtr*)objectRef + sizeof(IntPtr); } break; } _engineFunctions.Serialization_SerializeField((void*)serializationContext, type, fieldNameConverted, valueObjectConverted, fullTypeNameConverted); NativeMemory.Free(fieldNameConverted.Utf8Ptr); NativeMemory.Free(fullTypeNameConverted.Utf8Ptr); if (deleteValueObject) NativeMemory.Free(valueObjectConverted); } #endregion Custom engine call implementations }