using System; using System.IO; using System.Collections; using GlitchyEngine.Core; using GlitchyEngine.Math; using GlitchyEngine.Serialization; using GlitchyEngine.World; using System.Diagnostics; namespace GlitchyEngine.Scripting; using internal GlitchyEngine.Scripting; class EngineClasses { // TODO: Not all of these are actually ScriptClasses (actually none of them are...) private NewScriptClass s_EngineObject; private NewScriptClass s_ComponentRoot; private NewScriptClass s_EntityRoot; private EntityEditorWrapper s_EntityEditor; private EntitySerializerWrapper s_EntitySerializer; //private NewScriptClass s_SerializationContext; private NewScriptClass s_Collision2D; private NewScriptClass s_RunInEditModeAttribute; public NewScriptClass EngineObject => s_EngineObject; public NewScriptClass ComponentRoot => s_ComponentRoot; public NewScriptClass EntityRoot => s_EntityRoot; public EntityEditorWrapper EntityEditor => s_EntityEditor; public EntitySerializerWrapper EntitySerializer => s_EntitySerializer; public NewScriptClass Collision2D => s_Collision2D; public NewScriptClass RunInEditModeAttribute => s_RunInEditModeAttribute; public ~this() { ReleaseAndNullify(); } internal void ReleaseAndNullify() { DeleteAndNullify!(s_EngineObject); DeleteAndNullify!(s_EntityRoot); DeleteAndNullify!(s_ComponentRoot); DeleteAndNullify!(s_EntityEditor); DeleteAndNullify!(s_EntitySerializer); //ReleaseRefAndNullify!(s_SerializationContext); DeleteAndNullify!(s_Collision2D); DeleteAndNullify!(s_RunInEditModeAttribute); } internal void LoadClasses() { ReleaseAndNullify(); s_EngineObject = new NewScriptClass("GlitchyEngine.Core.EngineObject", .Empty, .None); s_EntityRoot = new NewScriptClass("GlitchyEngine.Core.Entity", .Empty, .None); s_ComponentRoot = new NewScriptClass("GlitchyEngine.Core.Component", .Empty, .None); // Editor classes s_EntityEditor = new EntityEditorWrapper(); s_EntitySerializer = new EntitySerializerWrapper(); s_Collision2D = new NewScriptClass("GlitchyEngine.Physics.Collision2D", .Empty, .None); // Attributes s_RunInEditModeAttribute = new NewScriptClass("GlitchyEngine.Editor.RunInEditModeAttribute", .Empty, .None); } } static class ScriptEngine { // TODO: Maybe make it possible to set the path to ScriptCore just like for the script project? // This would probably require a separate small project that just handles the script-engine interface. public const String ScriptCorePath = "Resources/Scripts/ScriptCore.dll"; private static Scene s_Context ~ _?.ReleaseRef(); private static EngineClasses _classes = new .() ~ delete _; public static EngineClasses Classes => _classes; private static Dictionary _entityScripts = new .() ~ DeleteDictionaryAndValues!(_); internal static Dictionary _entityScriptInstances = new .() ~ { for (var entry in _) { entry.value?.ReleaseRef(); } delete _; } public static Dictionary EntityClasses => _entityScripts; /// Gets or sets the current scene context for the runtime. public static Scene Context { get => s_Context; private set => SetReference!(s_Context, value); } private static FileSystemWatcher _userAssemblyWatcher ~ delete _; // TODO: This should be a global setting somewhere private static bool _debuggingEnabled = true; private static String _appAssemblyPath = new .() ~ delete _; public class ApplicationData { private bool _isEditor; private bool _isPlayer; private bool _isInEditMode; private bool _isInPlayMode; public bool IsEditor { get => _isEditor; set { _isEditor = value; if (_isEditor) _isPlayer = false; } } public bool IsPlayer { get => _isPlayer; set { _isPlayer = value; if (_isPlayer) _isEditor = false; } } public bool IsInEditMode { get => _isInEditMode; set { _isInEditMode = value; if (_isInEditMode) _isInPlayMode = false; } } public bool IsInPlayMode { get => _isInPlayMode; set { _isInPlayMode = value; if (_isInPlayMode) _isInEditMode = false; } } } private static ApplicationData _applicationData = new ApplicationData() ~ delete _; /// Contains information about the application context public static ApplicationData ApplicationInfo => _applicationData; public static void Init() { InitRuntime(); //InitMono(); LoadScriptAssemblies(); } /// Changes the path to the current app assembly. public static void SetAppAssemblyPath(StringView appAssemblyPath) { _appAssemblyPath.Set(appAssemblyPath); ReloadAssemblies(); } static void InitRuntime() { // TODO: We currently have the utility methods (e.g. assembly loading) in ScriptCore, // the problem is, that CoreCLR currently can't unload the initial assembly. // this means that we cannot easily reload changes to ScriptCore. // Since this basic infrastructure shouldn't really ever change we could make a tiny // library with only that enabling us to reload script core. CoreClrHelper.Init(ScriptCorePath); } /*static void InitMono() { Mono.mono_set_assemblies_path("mono/lib/4.5"); if (_debuggingEnabled) { char8*[2] options = .( "--debugger-agent=transport=dt_socket,address=127.0.0.1:2550,server=y,suspend=n,loglevel=3,logfile=MonoDebugger.log", "--soft-breakpoints" ); Mono.mono_jit_parse_options(options.Count, &options); Mono.mono_debug_init(.Mono); } s_RootDomain = Mono.mono_jit_init("GlitchyEngineJITRuntime"); Log.EngineLogger.Assert(s_RootDomain != null, "Failed to initialize mono root domain"); if (_debuggingEnabled) { Mono.mono_debug_domain_create(s_RootDomain); } Mono.mono_thread_set_main(Mono.mono_thread_current()); }*/ static bool _requestingReload = false; static void InitAssemblyWatcher() { // We don't need a file system watcher, if we have nothing to watch... if (!File.Exists(_appAssemblyPath)) return; String directory = scope .(); Path.GetDirectoryPath(_appAssemblyPath, directory); String fileName = scope .("*/"); Path.GetFileName(_appAssemblyPath, fileName); if (_userAssemblyWatcher == null) { _userAssemblyWatcher = new FileSystemWatcher(directory, fileName); _userAssemblyWatcher.OnChanged.Add(new (fileName) => { _userAssemblyWatcher.StopRaisingEvents(); if (_requestingReload) return; _requestingReload = true; Log.EngineLogger.Info("Script reload requested."); Application.Instance.InvokeOnMainThread(new () => { ReloadAssemblies(); _requestingReload = false; _userAssemblyWatcher.StartRaisingEvents(); return true; }); }); } _userAssemblyWatcher.StartRaisingEvents(); } static void LoadScriptAssemblies() { Debug.Profiler.ProfileFunction!(); // TODO: We should probably verify, that all required types actually exist. Classes.LoadClasses(); ScriptGlue.Init(); // TODO: Check if files exist if (File.Exists(_appAssemblyPath)) { List data = scope List(); File.ReadAll(_appAssemblyPath, data); List pdbData = scope List(); String pdbPath = scope .(); Path.ChangeExtension(_appAssemblyPath, ".pdb", pdbPath); File.ReadAll(pdbPath, pdbData); CoreClrHelper.LoadAppAssembly(data, pdbData); GetEntitiesFromAssemblies(); //ScriptGlue.RegisterManagedComponents(); } InitAssemblyWatcher(); } /// Starts the script runtime and sets the context scene. public static void StartRuntime(Scene context) { Debug.Assert(s_Context == null, "StartRuntime was called twice without StopRuntime in between!"); Context = context; ReloadAssemblies(); } /// Stopts the script runtime and disposes of all script instances. public static void StopRuntime() { if (Context == null) { // There should be nothing to do, if we have no context Debug.Assert(_entityScriptInstances.Count == 0, "There are script instances but no context scene."); return; } for (let (id, instance) in _entityScriptInstances) { instance?.ReleaseRef(); Entity entity = Context.GetEntityByID(id); // Remove Instance from script ScriptComponent* script = entity.GetComponent(); script.Instance = null; } _entityScriptInstances.Clear(); Context = null; } /// Instantiates the entities script components script and runs the constructor. /// Disposes of and replaces the old instance, if one exists. public static bool InitializeInstance(Entity entity, ScriptComponent* script) { NewScriptClass scriptClass = GetScriptClass(script.ScriptClassName); if (scriptClass == null) return false; UUID entityId = entity.UUID; script.Instance = new NewScriptInstance(entityId, scriptClass); script.Instance..ReleaseRef(); if (_entityScriptInstances.TryGetValue(entityId, let currentInstance)) currentInstance.ReleaseRef(); _entityScriptInstances[entityId] = script.Instance..AddRef(); script.Instance.Instantiate(); return true; } public static void DestroyInstance(Entity entity, ScriptComponent* script) { UUID entityId = entity.UUID; if (script.Instance != null) script.Instance = null; script.ScriptClassName = null; DestroyInstance(entityId); } public static void DestroyInstance(UUID entityId) { if (_entityScriptInstances.TryGetValue(entityId, let currentInstance)) currentInstance.ReleaseRef(); } internal static void UnregisterScriptInstance(UUID entityId) { _entityScriptInstances.Remove(entityId); } public enum ScriptMethods : uint32 { None = 0, OnCreate = 0x1, OnUpdate = 0x2, OnDestroy = 0x4, OnCollisionEnter2D = 0x8, } [Ordered] struct ScriptClassInfo { public char8* Name; public Guid Guid; public ScriptMethods Methods; public bool RunInEditMode; } private static void GetEntitiesFromAssemblies() { Debug.Profiler.ProfileFunction!(); CoreClrHelper.GetScriptClasses(let data, let entryCount); Span scriptClasses = .((.)data, entryCount); List newClasses = scope .(); ClearDictionaryAndDeleteValues!(_entityScripts); for (var entry in scriptClasses) { NewScriptClass scriptClass = new .(StringView(entry.Name), entry.Guid, entry.Methods, entry.RunInEditMode); newClasses.Add(scriptClass); } for (var scriptClass in newClasses) { _entityScripts.Add(scriptClass.FullName, scriptClass); } CoreClrHelper.FreeScriptClassNames(); } public static void ReloadAssemblies() { Debug.Profiler.ProfileFunction!(); Log.EngineLogger.Info("Reloading script assemblies."); ScriptInstanceSerializer contextSerializer = scope .(); contextSerializer.SerializeScriptInstances(); // TODO: Unload script assemblies, remove class handles, fire unload events? LoadScriptAssemblies(); { Debug.Profiler.ProfileScope!("Initialize Instances"); // We need to create a new instance for every entity for (let (id, scriptInstance) in _entityScriptInstances) { if (Context.GetEntityByID(id) case .Ok(let entity)) { ScriptComponent* script = entity.GetComponent(); InitializeInstance(entity, script); } else { Log.EngineLogger.AssertDebug(false, "Entities script was just serialized but the entity doesn't exist anymore."); } } } contextSerializer.DeserializeScriptInstances(); Log.EngineLogger.Info("Script assemblies reloaded!"); } public static void Shutdown() { /*Mono.mono_domain_set(s_RootDomain, false); Mono.mono_domain_unload(s_AppDomain); s_AppDomain = null; Mono.mono_jit_cleanup(s_RootDomain); s_RootDomain = null;*/ } /// Returns the script class with the given name, or null, if no such class exists. private static NewScriptClass GetScriptClass(StringView name) { EntityClasses.TryGetValue(name, let scriptClass); return scriptClass; } internal static void LogScriptException(ScriptException exception, UUID entityId) { String entityInfo = scope .(); if (entityId != .Zero && Context != null) { exception.EntityId = entityId; Result sourceEntity = Context.GetEntityByID(entityId); if (sourceEntity case .Ok(let e)) { entityInfo.AppendF($" ({e.Name} | {entityId})"); } } Log.ClientLogger.Error($"Exception \"{exception.FullName}\": \"{exception.Message}\"{entityInfo}\nStackTrace:\n{exception.StackTrace}", exception); } // TODO: Wrap like the other classes public static void ShowScriptEditor(Entity entity, ScriptComponent* scriptComponent) { if (scriptComponent.Instance == null) return; Classes.EntityEditor.ShowEntityEditor(scriptComponent.Instance, entity.UUID); } }