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.InteropServices; using System.Runtime.Loader; namespace GlitchyEngine; [StructLayout(LayoutKind.Sequential)] internal unsafe partial struct EngineFunctions { } /// /// All methods in here are glued to the ScriptGlue.bf in the engine. /// 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 } /// /// 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 = type.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly); 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); 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] 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] 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 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) { byte* fieldNameConverted = (byte*)Marshal.StringToCoTaskMemUTF8(fieldName); byte* fullTypeNameConverted = (byte*)Marshal.StringToCoTaskMemUTF8(fullTypeName); void* valueObjectConverted = null; bool deleteValueObject = false; switch (type) { case SerializationType.String: case SerializationType.Enum: if (valueObject is String stringValue) { valueObjectConverted = (void*)Marshal.StringToCoTaskMemUTF8(stringValue); deleteValueObject = true; } break; default: if (valueObject is not null) { void* p = &valueObject; // Skip Object Header (IntPtr) + Method Table (IntPtr) valueObjectConverted = (byte*)*(IntPtr*)p + sizeof(IntPtr); float i = *(float*)valueObjectConverted; } break; } _engineFunctions.Serialization_SerializeField((void*)serializationContext, type, fieldNameConverted, valueObjectConverted, fullTypeNameConverted); Marshal.FreeCoTaskMem((IntPtr)fieldNameConverted); Marshal.FreeCoTaskMem((IntPtr)fullTypeNameConverted); if (deleteValueObject) Marshal.FreeCoTaskMem((IntPtr)valueObjectConverted); } }