mirror of
https://github.com/aharabada/glitchy-engine-beef.git
synced 2026-09-05 13:01:52 +00:00
523 lines
12 KiB
Beef
523 lines
12 KiB
Beef
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<StringView, NewScriptClass> _entityScripts = new .() ~ DeleteDictionaryAndValues!(_);
|
|
|
|
internal static Dictionary<UUID, NewScriptInstance> _entityScriptInstances = new .() ~ {
|
|
for (var entry in _)
|
|
{
|
|
entry.value?.ReleaseRef();
|
|
}
|
|
delete _;
|
|
}
|
|
|
|
public static Dictionary<StringView, NewScriptClass> 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<uint8> data = scope List<uint8>();
|
|
File.ReadAll(_appAssemblyPath, data);
|
|
|
|
List<uint8> pdbData = scope List<uint8>();
|
|
|
|
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<ScriptComponent>();
|
|
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<ScriptClassInfo> scriptClasses = .((.)data, entryCount);
|
|
|
|
List<NewScriptClass> 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<ScriptComponent>();
|
|
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<Entity> 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);
|
|
}
|
|
}
|