18 Commits
Author SHA1 Message Date
Simon Lübeß c7fb9af0c7 Validate input of edit fields for type char 2026-09-13 22:41:12 +02:00
Simon Lübeß fff1abf012 Fixed integer displays in script editor 2026-09-13 21:56:17 +02:00
Simon Lübeß 8e9efbb367 Remove unnecessary PlayMode enum 2026-09-13 21:16:09 +02:00
Simon Lübeß 8244033ac5 Skip unnecessary (de-)serializations when reloading script assembly 2026-09-13 21:11:35 +02:00
Simon Lübeß e3619ae93c Properly unload script assembly when reloading 2026-09-13 17:45:12 +02:00
Simon Lübeß 0087bca27b Log error when script assembly doesn't exist 2026-09-12 15:19:55 +02:00
Simon Lübeß 9119613e62 StaticInit ScriptEngine after FileSystemWatcher 2026-09-12 15:18:01 +02:00
Simon Lübeß 1636f3a5d0 Fix serialization of EngineObjectReferences 2026-09-12 15:10:14 +02:00
Simon Lübeß 886d2760bc Update ScriptCore to DotNet 10 2026-09-12 15:06:14 +02:00
Simon Lübeß 6da69aad03 Fixed serialization of engine object references 2026-09-03 22:37:43 +02:00
Simon Lübeß 6c184aafe0 Correctly use ScriptGlue functions for Input queries 2026-09-03 22:36:15 +02:00
Simon Lübeß 5c18f61da4 Fixed Entity_GetScriptInstance 2026-09-03 22:35:43 +02:00
Simon Lübeß 1435dac4d6 Fixed exception logging and entity id of messages 2026-09-03 22:35:14 +02:00
Simon Lübeß abf7a01917 More robust "recent projects" deduplication 2026-08-23 12:54:18 +02:00
Simon Lübeß c8d63a9c36 More robust csproj-referencing
also add other relevant project-files to the solution
2026-08-22 18:45:29 +02:00
Simon Lübeß 9ccaf8977a Be a more patient with Visual Studio when moving the cursor 2026-08-22 18:38:11 +02:00
Simon Lübeß e758d6fd09 Allow jumping to columns in IDE 2026-08-22 18:15:38 +02:00
Simon Lübeß 1cc604cd51 Only write to solution and project files when they changed 2026-08-17 22:14:31 +02:00
29 changed files with 970 additions and 332 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ IconFile = "$(ProjectDir)/Resources/Textures/GlitchyEngineIcon.ico"
[Configs.Debug.Win64] [Configs.Debug.Win64]
PostBuildCmds = ["CopyFilesIfNewer(\"$(WorkspaceDir)/bin/vswhere.exe\", \"$(TargetDir)\")"] PostBuildCmds = ["CopyFilesIfNewer(\"$(WorkspaceDir)/bin/vswhere.exe\", \"$(TargetDir)\")"]
DebugCommandArguments = "\"D:\\Development\\Git\\SingleStateToOrbit\"" DebugCommandArguments = "\"$(ProjectDir)/../GlitchyEngine.Test/content/TestProject\""
PreprocessorMacros = ["DEBUG", "IMGUI"] PreprocessorMacros = ["DEBUG", "IMGUI"]
[Configs.Release.Win64] [Configs.Release.Win64]
@@ -13,9 +13,6 @@ namespace GlitchyEditor.CodeEditors;
/// Runs on its own worker thread so the editor never blocks on a busy or starting Visual Studio. /// Runs on its own worker thread so the editor never blocks on a busy or starting Visual Studio.
class DteOpenRequest class DteOpenRequest
{ {
private const int ColdStartMaxPolls = 120;
private const int ColdStartPollIntervalMs = 500;
private append String _solutionPath = .(); private append String _solutionPath = .();
private append String _devenvPath = .(); private append String _devenvPath = .();
private append String _fileName = .(); private append String _fileName = .();
@@ -40,6 +37,7 @@ class DteOpenRequest
} }
public int LineNumber; public int LineNumber;
public int ColumnNumber;
private static int32 sWorkerActive = 0; private static int32 sWorkerActive = 0;
@@ -83,7 +81,7 @@ class DteOpenRequest
if (FileName.IsEmpty) if (FileName.IsEmpty)
VisualStudioDte.ActivateMainWindow(dte); VisualStudioDte.ActivateMainWindow(dte);
else else
VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber).IgnoreError(); VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber, ColumnNumber).IgnoreError();
delete dte; delete dte;
@@ -107,13 +105,13 @@ class DteOpenRequest
// Wait for the new instance to finish loading the solution (Solution.FullName stays empty // Wait for the new instance to finish loading the solution (Solution.FullName stays empty
// until then), then open the file in it. // until then), then open the file in it.
for (int i < ColdStartMaxPolls) for (int i < VisualStudioDte.MaxPolls)
{ {
Thread.Sleep(ColdStartPollIntervalMs); Thread.Sleep(VisualStudioDte.PollIntervalMs);
if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let startedDte)) if (VisualStudioDte.FindRunningInstance(SolutionPath) case .Ok(let startedDte))
{ {
VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber).IgnoreError(); VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber, ColumnNumber).IgnoreError();
delete startedDte; delete startedDte;
return; return;
@@ -14,6 +14,9 @@ namespace GlitchyEditor.CodeEditors;
/// All methods must be called from a thread that is initialized for COM (CoInitializeEx). /// All methods must be called from a thread that is initialized for COM (CoInitializeEx).
static class VisualStudioDte static class VisualStudioDte
{ {
public const int MaxPolls = 120;
public const int PollIntervalMs = 500;
private const String MonikerPrefix = "!VisualStudio.DTE"; private const String MonikerPrefix = "!VisualStudio.DTE";
/// EnvDTE.Constants.vsViewKindCode /// EnvDTE.Constants.vsViewKindCode
private const String ViewKindCode = "{7651A701-06E5-11D1-8EBD-00A0C90F26EA}"; private const String ViewKindCode = "{7651A701-06E5-11D1-8EBD-00A0C90F26EA}";
@@ -107,7 +110,7 @@ static class VisualStudioDte
/// Opens the given file in the code editor of the given Visual Studio instance and /// Opens the given file in the code editor of the given Visual Studio instance and
/// jumps to lineNumber (1-based, ignored if <= 0). /// jumps to lineNumber (1-based, ignored if <= 0).
public static Result<void> OpenFileAtLine(ComDispatch dte, StringView fileName, int lineNumber) public static Result<void> OpenFileAtLine(ComDispatch dte, StringView fileName, int lineNumber, int columnNumber)
{ {
ComDispatch itemOperations = Try!(dte.GetObjectProperty("ItemOperations")); ComDispatch itemOperations = Try!(dte.GetObjectProperty("ItemOperations"));
defer delete itemOperations; defer delete itemOperations;
@@ -125,7 +128,7 @@ static class VisualStudioDte
} }
if (lineNumber > 0) if (lineNumber > 0)
GoToLine(dte, lineNumber); GoToLine(dte, lineNumber, columnNumber);
ActivateMainWindow(dte); ActivateMainWindow(dte);
@@ -198,18 +201,18 @@ static class VisualStudioDte
return path; return path;
} }
private static void GoToLine(ComDispatch dte, int lineNumber) private static void GoToLine(ComDispatch dte, int lineNumber, int columnNumber)
{ {
ComDispatch document = null; ComDispatch document = null;
defer { delete document; } defer { delete document; }
// Directly after OpenFile the ActiveDocument might not be set yet, so retry for a bit. // Directly after OpenFile the ActiveDocument might not be set yet, so retry for a bit.
for (int i < 10) for (int i < MaxPolls)
{ {
if (dte.GetObjectProperty("ActiveDocument") case .Ok(out document)) if (dte.GetObjectProperty("ActiveDocument") case .Ok(out document))
break; break;
Thread.Sleep(100); Thread.Sleep(PollIntervalMs);
} }
if (document == null) if (document == null)
@@ -224,9 +227,14 @@ static class VisualStudioDte
return; return;
} }
VARIANT[2] gotoLineArgs = .(VARIANT.FromInt32((int32)lineNumber), VARIANT.FromBool(false)); /*VARIANT[2] gotoLineArgs = .(VARIANT.FromInt32((int32)lineNumber), VARIANT.FromBool(false));
if (selection.InvokeMethod("GotoLine", gotoLineArgs) case .Ok(var gotoResult)) if (selection.InvokeMethod("GotoLine", gotoLineArgs) case .Ok(var gotoResult))
VariantClear(&gotoResult);*/
VARIANT[3] gotoLineArgs = .(VARIANT.FromInt32((int32)lineNumber), VARIANT.FromInt32((int32)columnNumber), VARIANT.FromBool(false));
if (selection.InvokeMethod("MoveTo", gotoLineArgs) case .Ok(var gotoResult))
VariantClear(&gotoResult); VariantClear(&gotoResult);
delete selection; delete selection;
+7 -1
View File
@@ -1,9 +1,15 @@
using System; using System;
namespace GlitchyEditor.CodeEditors; namespace GlitchyEditor.CodeEditors;
interface IIdeAdapter interface IIdeAdapter
{ {
void OpenScript(StringView fileName, int lineNumber = 0); /// Opens the current script solution and the specified script file in the IDE. Allows to specify a line number and a column that the cursor will be moved to.
/// @param fileName The absolute path to the script file. Can be null to just open the script project (Note: the same thing can be achieved by using @see OpenScriptProject).
/// @param lineNumber The line number that the cursor will be placed at (1-based, so 1 is first line).
/// @param lineNumber The column number that the cursor will be placed at (1-based, so 1 is the start of the line).
void OpenScript(StringView fileName, int lineNumber = 0, int columnNumber = 0);
/// Opens the current script solution in the IDE.
void OpenScriptProject(); void OpenScriptProject();
} }
@@ -21,14 +21,15 @@ class RiderIdeAdapter : IIdeAdapter
_ideInstallation = ideInstallation; _ideInstallation = ideInstallation;
} }
public void OpenScript(StringView fileName, int lineNumber) public void OpenScript(StringView fileName, int lineNumber, int columnNumber)
{ {
String solutionPath = scope .(); String solutionPath = scope .();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
ProcessStartInfo startInfo = scope .(); ProcessStartInfo startInfo = scope .();
startInfo.SetFileName(_ideInstallation.Path); startInfo.SetFileName(_ideInstallation.Path);
startInfo.SetArguments(scope $"{solutionPath} --line {lineNumber} {fileName}"); // For rider columns appear to be 0-indexed
startInfo.SetArguments(scope $"\"{solutionPath}\" --line {lineNumber} --column {columnNumber - 1} \"{fileName}\"");
scope SpawnedProcess().Start(startInfo); scope SpawnedProcess().Start(startInfo);
} }
@@ -18,17 +18,17 @@ class VisualStudioIdeAdapter : IIdeAdapter
_ideInstallation = ideInstallation; _ideInstallation = ideInstallation;
} }
public void OpenScript(StringView fileName, int lineNumber) public void OpenScript(StringView fileName, int lineNumber, int columnNumber)
{ {
Open(fileName, lineNumber); Open(fileName, lineNumber, columnNumber);
} }
public void OpenScriptProject() public void OpenScriptProject()
{ {
Open(null, 0); Open(null, 0, 0);
} }
private void Open(StringView fileName, int lineNumber) private void Open(StringView fileName, int lineNumber, int columnNumber)
{ {
String solutionPath = scope String(); String solutionPath = scope String();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath); Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
@@ -57,6 +57,7 @@ class VisualStudioIdeAdapter : IIdeAdapter
request.DevenvPath = _ideInstallation.Path; request.DevenvPath = _ideInstallation.Path;
request.FileName = fileName; request.FileName = fileName;
request.LineNumber = lineNumber; request.LineNumber = lineNumber;
request.ColumnNumber = columnNumber;
DteOpenRequest.Start(request); DteOpenRequest.Start(request);
#else #else
@@ -66,6 +66,8 @@ namespace GlitchyEditor.EditWindows
{ {
_entityToHighlight = e; _entityToHighlight = e;
SelectEntity(e, true);
Entity walker = _entityToHighlight; Entity walker = _entityToHighlight;
while (walker.Parent != null) while (walker.Parent != null)
+32 -13
View File
@@ -8,6 +8,7 @@ using GlitchyEngine.Scripting;
using GlitchyEngine.Renderer; using GlitchyEngine.Renderer;
using GlitchyEngine; using GlitchyEngine;
using GlitchyEditor.CodeEditors; using GlitchyEditor.CodeEditors;
using System.IO;
namespace GlitchyEditor.EditWindows; namespace GlitchyEditor.EditWindows;
@@ -295,12 +296,30 @@ class LogWindow : EditorWindow
{ {
if (ImGui.CollapsingHeader(message.Message.Ptr)) if (ImGui.CollapsingHeader(message.Message.Ptr))
{ {
// Show the native to managed entry point only if we show engine messages String lineBuffer = scope .();
for (StackFrameInfo frameInfo in message.Source.Exception.StackTrace.Frames)
{
String relativeSourcePath = scope .(frameInfo.FileName);
if (_showEngineMessages) relativeSourcePath.Replace(Editor.Instance.ContentManager.ResourcesDirectory, "Resources");
ImGui.TextUnformatted(message.Source.Exception.StackTrace); relativeSourcePath.Replace(Editor.Instance.ContentManager.AssetDirectory, "Assets");
else
ImGui.TextUnformatted(message.Source.Exception.CleanStackTrace); //Path.GetRelativePath(frameInfo.FileName, Editor.Instance.CurrentProject.WorkspacePath, relativeSourcePath);
StackFrameInfo sfi = scope .(relativeSourcePath, frameInfo.MethodSignature, frameInfo.Line, frameInfo.Column);
//_editor.CurrentProject.PathInProject();
lineBuffer.Clear();
lineBuffer.SetF($" {sfi}");
float width = ImGui.CalcTextSize(lineBuffer.CStr()).x;
if (ImGui.Selectable(lineBuffer, false, .None, .(width, 0)))
{
// TODO: Columns?
Editor.Instance.IdeAdapter.OpenScript(frameInfo.FileName, frameInfo.Line, frameInfo.Column);
}
}
ImGui.NewLine(); ImGui.NewLine();
} }
@@ -319,6 +338,13 @@ class LogWindow : EditorWindow
{ {
Editor.Instance.IdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber); Editor.Instance.IdeAdapter.OpenScript(message.Source.MessageOrigin.FileName, message.Source.MessageOrigin.LineNumber);
} }
else if (message.Source.Exception != null)
{
// TODO: Column?
let firstFrame = message.Source.Exception.StackTrace.Frames[0];
Editor.Instance.IdeAdapter.OpenScript(firstFrame.FileName, firstFrame.Line);
//message.Source.Exception.StackTrace
}
} }
// Dont show the counter if we only have one message. // Dont show the counter if we only have one message.
@@ -373,15 +399,8 @@ class LogWindow : EditorWindow
public void LogException(DateTime timestamp, ScriptException exception) public void LogException(DateTime timestamp, ScriptException exception)
{ {
StringView firstLine = exception.StackTrace;
int firstInIndex = exception.StackTrace.IndexOf("\n");
if (firstInIndex != -1)
firstLine = firstLine.Substring(0, firstInIndex);
String message = scope .(128); String message = scope .(128);
message.AppendF($"Exception: \"{exception.FullName}\" | Message: \"{exception.Message}\" {firstLine}\0"); message.AppendF($"Exception: \"{exception.FullName}\" | Message: \"{exception.Message}\" {exception.StackTrace.Frames[0]}\0");
// TODO: are mono exceptions never engine only? // TODO: are mono exceptions never engine only?
LogMessage logMessage = new LogMessage(timestamp, message, .Error, new MessageSource(){Entity = exception.EntityId, Exception = exception, IsEngineMessage = false}); LogMessage logMessage = new LogMessage(timestamp, message, .Error, new MessageSource(){Entity = exception.EntityId, Exception = exception, IsEngineMessage = false});
+56 -57
View File
@@ -75,14 +75,6 @@ namespace GlitchyEditor
private float _fixedTimestep = 1.0f / 60.0f; private float _fixedTimestep = 1.0f / 60.0f;
append List<(String Name, String Path)> _recentScenePaths = .() ~ {
for (var item in _)
{
delete item.Name;
delete item.Path;
}
};
private GameTime _simulationGameTime = new .() ~ delete _; private GameTime _simulationGameTime = new .() ~ delete _;
/// Gets or sets the path of the current scene. /// Gets or sets the path of the current scene.
@@ -720,6 +712,8 @@ namespace GlitchyEditor
private void CloseCurrentProject() private void CloseCurrentProject()
{ {
Log.EngineLogger.Trace("Closing current Project...");
CloseCurrentScene(); CloseCurrentScene();
SetEditorScene(null); SetEditorScene(null);
@@ -729,6 +723,8 @@ namespace GlitchyEditor
// TODO: Do actual work here! // TODO: Do actual work here!
delete _currentProject; delete _currentProject;
_currentProject = null; _currentProject = null;
Log.EngineLogger.Trace("Project closed.");
} }
private Result<void> LoadAndOpenProject(StringView workspacePath) private Result<void> LoadAndOpenProject(StringView workspacePath)
@@ -752,29 +748,43 @@ namespace GlitchyEditor
CloseCurrentProject(); CloseCurrentProject();
Log.EngineLogger.Trace("Opening Project...");
_currentProject = project; _currentProject = project;
_editor.CurrentProject = _currentProject; _editor.CurrentProject = _currentProject;
Log.EngineLogger.Trace("Preparing asset manager...");
String appAssemblyPath = scope String(); String appAssemblyPath = scope String();
_contentManager.SetAssetCacheDirectory(_currentProject.GetScopedPath!(".cache")); _contentManager.SetAssetCacheDirectory(_currentProject.GetScopedPath!(".cache"));
_contentManager.SetAssetDirectory(_currentProject.AssetsFolder); _contentManager.SetAssetDirectory(_currentProject.AssetsFolder);
_currentProject.PathInProject(appAssemblyPath, scope $".cache/bin/{_currentProject.Name}.dll"); _currentProject.PathInProject(appAssemblyPath, scope $".cache/bin/{_currentProject.Name}.dll");
ScriptEngine.SetAppAssemblyPath(appAssemblyPath); Log.EngineLogger.Trace("Loading script assembly...");
ScriptEngine.SetAppAssemblyPath(appAssemblyPath, retainScriptData: false);
if (!_currentProject.UserSettings.LastOpenedScene.IsWhiteSpace) if (!_currentProject.UserSettings.LastOpenedScene.IsWhiteSpace)
{ {
String lastSceneFile = _currentProject.GetScopedPath!(_currentProject.UserSettings.LastOpenedScene); String lastSceneFile = _currentProject.GetScopedPath!(_currentProject.UserSettings.LastOpenedScene);
if (File.Exists(lastSceneFile)) if (File.Exists(lastSceneFile))
{
Log.EngineLogger.Trace("Opening last opened scene...");
LoadSceneFile(lastSceneFile); LoadSceneFile(lastSceneFile);
else
CreateAndOpenNewScene();
} }
else else
{
Log.EngineLogger.Trace("Opening new scene...");
CreateAndOpenNewScene(); CreateAndOpenNewScene();
}
}
else
{
Log.EngineLogger.Trace("Opening new scene...");
CreateAndOpenNewScene();
}
UpdateWindowTitle(); UpdateWindowTitle();
@@ -784,6 +794,8 @@ namespace GlitchyEditor
EditorApp.Instance.Settings.Save(); EditorApp.Instance.Settings.Save();
} }
Log.EngineLogger.Trace("Project opened.");
return .Ok; return .Ok;
} }
@@ -808,28 +820,35 @@ namespace GlitchyEditor
append ScriptInstanceSerializer _scriptSerializer = .(); append ScriptInstanceSerializer _scriptSerializer = .();
/// Starts the play mode for the current scene /// Starts the play mode for the current scene
private void OnScenePlay() private void OnScenePlay(SceneState newSceneState)
{ {
Debug.Profiler.ProfileFunction!(); Debug.Profiler.ProfileFunction!();
Runtime.Assert(newSceneState == .Play || newSceneState == .Simulate, "newSceneState must either be Play or Simulate");
Log.EngineLogger.AssertDebug(_scriptSerializer.SerializedObjectCount == 0, "Somehow some entities are serialized."); Log.EngineLogger.AssertDebug(_scriptSerializer.SerializedObjectCount == 0, "Somehow some entities are serialized.");
if (EditorApp.Instance.Settings.EditorSettings.ClearLogOnPlay) if (EditorApp.Instance.Settings.EditorSettings.ClearLogOnPlay)
Editor.Instance.LogWindow.ClearLog(); Editor.Instance.LogWindow.ClearLog();
/// Save the in memory state of the script intances so se can recreate it after stopping play/simulation
_scriptSerializer.SerializeScriptInstances(); _scriptSerializer.SerializeScriptInstances();
_editor.SceneViewportWindow.EditorMode = false; _editor.SceneViewportWindow.EditorMode = false;
_sceneState = .Play; _sceneState = newSceneState;
using (Scene runtimeScene = new Scene()) using (Scene runtimeScene = new Scene())
{ {
_editorScene.CopyTo(runtimeScene, true); // In simulation mode we don't want any scripts to run, just physics.
_editorScene.CopyTo(runtimeScene, _sceneState == .Play);
SetActiveScene(runtimeScene, startRuntime: true, startSimulation: true, newPlayMode: .Play); SetActiveScene(runtimeScene, startRuntime: (_sceneState == .Play), startSimulation: true, _sceneState);
if (_sceneState == .Play)
{
_scriptSerializer.DeserializeScriptInstances(); _scriptSerializer.DeserializeScriptInstances();
} }
}
_editor.CurrentScene = _activeScene; _editor.CurrentScene = _activeScene;
@@ -837,35 +856,26 @@ namespace GlitchyEditor
SwitchToPlayWindow(); SwitchToPlayWindow();
} }
enum PlayMode
{
Play,
Editor,
Simulation
}
/// Activates the given scene. /// Activates the given scene.
/// @param scene The scene to be activated. /// @param scene The scene to be activated.
/// @param startRuntime If set to true, the script runtime will be initialized for the given scene. /// @param startRuntime If set to true, the script runtime will be initialized for the given scene.
/// @param startSimulation If set to true, the physics simulation will be initialized for the given scene. /// @param startSimulation If set to true, the physics simulation will be initialized for the given scene.
private void SetActiveScene(Scene scene, bool startRuntime, bool startSimulation, PlayMode? newPlayMode = null) /// @param newSceneState The new scene state.
private void SetActiveScene(Scene scene, bool startRuntime, bool startSimulation, SceneState newSceneState)
{ {
_activeScene?.Stop(); _activeScene?.Stop();
if (newPlayMode != null) switch (newSceneState)
{
switch (newPlayMode.Value)
{ {
case .Play: case .Play:
ScriptEngine.ApplicationInfo.IsInPlayMode = true; ScriptEngine.ApplicationInfo.IsInPlayMode = true;
case .Editor: case .Edit:
ScriptEngine.ApplicationInfo.IsInEditMode = true; ScriptEngine.ApplicationInfo.IsInEditMode = true;
case .Simulation: case .Simulate:
// No scripts in Simulation // No scripts in Simulation
ScriptEngine.ApplicationInfo.IsInEditMode = false; ScriptEngine.ApplicationInfo.IsInEditMode = false;
ScriptEngine.ApplicationInfo.IsInPlayMode = false; ScriptEngine.ApplicationInfo.IsInPlayMode = false;
} }
}
if (scene != null) if (scene != null)
{ {
@@ -875,25 +885,6 @@ namespace GlitchyEditor
SetReference!(_activeScene, scene); SetReference!(_activeScene, scene);
} }
/// Starts the physics simulation mode for the current scene
private void OnSceneSimulate()
{
_editor.SceneViewportWindow.EditorMode = false;
_sceneState = .Simulate;
using (Scene simulationScene = new Scene())
{
_editorScene.CopyTo(simulationScene, false);
SetActiveScene(simulationScene, startRuntime: false, startSimulation: true, newPlayMode: .Simulation);
}
_editor.CurrentScene = _activeScene;
if (EditorApp.Instance.Settings.EditorSettings.SwitchToPlayerOnSimulate)
SwitchToPlayWindow();
}
/// Pauses the scene /// Pauses the scene
private void OnScenePause() private void OnScenePause()
{ {
@@ -924,13 +915,13 @@ namespace GlitchyEditor
if (_activeScene == null) if (_activeScene == null)
return; return;
SetActiveScene(_editorScene, startRuntime: true, startSimulation: false, newPlayMode: .Editor); Log.EngineLogger.Trace("Stopping current scene and returing to edit mode...");
// TODO: _editorScene.DeserializeScripts(scriptData);
_editor.SceneViewportWindow.EditorMode = true; _editor.SceneViewportWindow.EditorMode = true;
_sceneState = .Edit; _sceneState = .Edit;
SetActiveScene(_editorScene, startRuntime: true, startSimulation: false, _sceneState);
_editor.CurrentScene = _activeScene; _editor.CurrentScene = _activeScene;
_isPaused = false; _isPaused = false;
@@ -943,14 +934,19 @@ namespace GlitchyEditor
* into "Editor"-mode (because Game-Mode works on a copy of the scene). * into "Editor"-mode (because Game-Mode works on a copy of the scene).
*/ */
GameViewportSizeChanged(null, _editor.GameViewportWindow.RenderedViewportSize); GameViewportSizeChanged(null, _editor.GameViewportWindow.RenderedViewportSize);
if (_scriptSerializer.SerializedObjectCount != 0)
{
// Reconstruct state before play
_scriptSerializer.DeserializeScriptInstances();
_scriptSerializer.Clear();
}
} }
if (EditorApp.Instance.Settings.EditorSettings.SwitchToEditorOnStop) if (EditorApp.Instance.Settings.EditorSettings.SwitchToEditorOnStop)
SwitchToEditorWindow(); SwitchToEditorWindow();
// Reconstruct state before play Log.EngineLogger.Trace("Switched to edit mode.");
_scriptSerializer.DeserializeScriptInstances();
_scriptSerializer.Clear();
} }
/// Stops the scene and cleans up the subsystems to allow loading another scene. /// Stops the scene and cleans up the subsystems to allow loading another scene.
@@ -983,6 +979,8 @@ namespace GlitchyEditor
/// Sets the current editor scene /// Sets the current editor scene
private void SetEditorScene(Scene scene, ScriptInstanceSerializer serializedObjects = null) private void SetEditorScene(Scene scene, ScriptInstanceSerializer serializedObjects = null)
{ {
Log.EngineLogger.Trace($"Switching to scene \"{(scene?.Name ?? "<none>")}\"...");
_editorScene?.Stop(); _editorScene?.Stop();
SetReference!(_editorScene, scene); SetReference!(_editorScene, scene);
@@ -1002,6 +1000,7 @@ namespace GlitchyEditor
} }
UpdateWindowTitle(); UpdateWindowTitle();
Log.EngineLogger.Trace($"Switched scene to \"{(scene?.Name ?? "<none>")}\".");
} }
/// Creates a new default scene. /// Creates a new default scene.
@@ -1239,7 +1238,7 @@ namespace GlitchyEditor
ImGui.PushID(0); ImGui.PushID(0);
if (ImGui.ImageButton("", _editorIcons.Play, .(size, size), .Zero, .Ones)) if (ImGui.ImageButton("", _editorIcons.Play, .(size, size), .Zero, .Ones))
OnScenePlay(); OnScenePlay(.Play);
ImGui.PopID(); ImGui.PopID();
@@ -1250,7 +1249,7 @@ namespace GlitchyEditor
ImGui.PushID(1); ImGui.PushID(1);
if (ImGui.ImageButton("", _editorIcons.Simulate, .(size, size), .Zero, .Ones)) if (ImGui.ImageButton("", _editorIcons.Simulate, .(size, size), .Zero, .Ones))
OnSceneSimulate(); OnScenePlay(.Simulate);
ImGui.PopID(); ImGui.PopID();
@@ -1522,7 +1521,7 @@ namespace GlitchyEditor
if(ImGui.BeginMenu("Tools", true)) if(ImGui.BeginMenu("Tools", true))
{ {
if(ImGui.MenuItem("Reload Scripts")) if(ImGui.MenuItem("Reload Scripts"))
ScriptEngine.ReloadAssemblies(); ScriptEngine.ReloadAssemblies(retainScriptData: true);
#if GE_EDITOR_IMGUI_DEMO #if GE_EDITOR_IMGUI_DEMO
ImGui.Checkbox("Show ImGui Demo", &_showImguiDemoWindow); ImGui.Checkbox("Show ImGui Demo", &_showImguiDemoWindow);
+1 -1
View File
@@ -139,7 +139,7 @@ public class EditorLogger : Logger
} }
else if (args.Count > 0 && messageOrigin != null) else if (args.Count > 0 && messageOrigin != null)
{ {
Editor.Instance.LogWindow.Log(timestamp, level, message, new .() {IsEngineMessage = IsEngineLogger, MessageOrigin = new MessageOrigin(messageOrigin.FileName, messageOrigin.LineNumber)}); Editor.Instance.LogWindow.Log(timestamp, level, message, new .() {Entity = (messageOrigin.EntityId != .Zero ? messageOrigin.EntityId : null), IsEngineMessage = IsEngineLogger, MessageOrigin = new MessageOrigin(messageOrigin.EntityId, messageOrigin.FileName, messageOrigin.CallerMemberName, messageOrigin.LineNumber, messageOrigin.ColumnNumber)});
} }
else else
{ {
+135 -35
View File
@@ -36,7 +36,10 @@ class Project
[AllowAppend] [AllowAppend]
private this(StringView workspacePath) private this(StringView workspacePath)
{ {
_workspacePath = new String(workspacePath); _workspacePath = new String(workspacePath.Length);
Path.GetFullPath(workspacePath, _workspacePath);
if (_workspacePath.EndsWith(Path.DirectorySeparatorChar))
_workspacePath.RemoveFromEnd(1);
_assetsFolder = new String(); _assetsFolder = new String();
PathInProject(_assetsFolder, "Assets"); PathInProject(_assetsFolder, "Assets");
@@ -80,6 +83,27 @@ class Project
Path.ToActualPath(outTarget); Path.ToActualPath(outTarget);
} }
private static void GetScriptCoreGeneratorProjectFilePath(String outTarget)
{
Directory.GetCurrentDirectory(outTarget);
Path.Combine(outTarget, "../ScriptCoreGenerator/ScriptCoreGenerator.csproj");
Path.ToActualPath(outTarget);
}
private static void GetScriptCoreGeneratorTestProjectFilePath(String outTarget)
{
Directory.GetCurrentDirectory(outTarget);
Path.Combine(outTarget, "../ScriptCoreGenerator.Test/ScriptCoreGenerator.Test.csproj");
Path.ToActualPath(outTarget);
}
private static void GetImGuiDotNetProjectFilePath(String outTarget)
{
Directory.GetCurrentDirectory(outTarget);
Path.Combine(outTarget, "../vendor/ImGui.NET/src/ImGui.NET/ImGui.NET.csproj");
Path.ToActualPath(outTarget);
}
/// Loads or creates the user specific settings for this project. /// Loads or creates the user specific settings for this project.
private void InitUserSettings() private void InitUserSettings()
{ {
@@ -274,7 +298,8 @@ class Project
return project; return project;
} }
/// If necessary adds or removes the reference to ScriptCore.csproj from the solution (.slnx) file. /// If missing adds the reference to ScriptCore and other script dependencies from the solution (.slnx) file.
/// @param referenceScriptCoreProject If true, ScriptCore and other dependencies will be referenced using their .csproj-files. Otherwise the ScriptCore-dll will be referenced.
private Result<void> FixupScriptSolutionFile(bool referenceScriptCoreProject) private Result<void> FixupScriptSolutionFile(bool referenceScriptCoreProject)
{ {
String solutionPath = scope .(); String solutionPath = scope .();
@@ -293,38 +318,27 @@ class Project
solution.LoadFromString(solutionFileContent); solution.LoadFromString(solutionFileContent);
XmlNode scriptCoreProjectReference = null; bool changedContent = false;
for (XmlNode projectNode in solution.DocumentElement.EnumerateNodes("Project", recursive: true))
{
if (projectNode.TryGetAttribute("Path", let includeAttribute))
{
if (includeAttribute.Value.Contains("ScriptCore.csproj", true))
{
scriptCoreProjectReference = projectNode;
break;
}
}
}
if (referenceScriptCoreProject) String scriptCoreFilePath = scope .();
{ GetScriptCoreProjectFilePath(scriptCoreFilePath);
Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Solution."); changedContent |= Try!(AddProjectToSolution(solution, scriptCoreFilePath, referenceScriptCoreProject));
if (scriptCoreProjectReference == null) String scriptCoreGeneratorFilePath = scope .();
{ GetScriptCoreGeneratorProjectFilePath(scriptCoreGeneratorFilePath);
Log.EngineLogger.Warning("No reference to ScriptCore.csproj found. Adding..."); changedContent |= Try!(AddProjectToSolution(solution, scriptCoreGeneratorFilePath, referenceScriptCoreProject));
scriptCoreProjectReference = solution.DocumentElement.AddChild("Project");
}
String projectFilePath = scope .(); String scriptCoreGeneratorTestFilePath = scope .();
GetScriptCoreProjectFilePath(projectFilePath); GetScriptCoreGeneratorTestProjectFilePath(scriptCoreGeneratorTestFilePath);
scriptCoreProjectReference["Path"] = projectFilePath; changedContent |= Try!(AddProjectToSolution(solution, scriptCoreGeneratorTestFilePath, referenceScriptCoreProject));
}
else if (scriptCoreProjectReference != null) String imGuiDotNetFilePath = scope .();
GetImGuiDotNetProjectFilePath(imGuiDotNetFilePath);
changedContent |= Try!(AddProjectToSolution(solution, imGuiDotNetFilePath, referenceScriptCoreProject));
if (changedContent)
{ {
Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing..."); Log.EngineLogger.Info("Script solution file updated, saving...");
scriptCoreProjectReference.RemoveFromParent(true);
}
String modifiedSolution = new String(solutionFileContent.Length * 2); String modifiedSolution = new String(solutionFileContent.Length * 2);
defer delete modifiedSolution; defer delete modifiedSolution;
@@ -335,10 +349,63 @@ class Project
Log.EngineLogger.Error($"Failed to modify {solutionPath}"); Log.EngineLogger.Error($"Failed to modify {solutionPath}");
return .Err; return .Err;
} }
}
else
{
Log.EngineLogger.Info("Script solution file already up to date.");
}
return .Ok; return .Ok;
} }
private Result<bool> AddProjectToSolution(Xml solution, StringView projectFilePath, bool referenceScriptCoreProject)
{
String projectName = scope .();
Path.GetFileName(projectFilePath, projectName);
XmlNode projectReference = null;
for (XmlNode projectNode in solution.DocumentElement.EnumerateNodes("Project", recursive: true))
{
if (projectNode.TryGetAttribute("Path", let includeAttribute))
{
if (includeAttribute.Value.Contains(projectName, true))
{
projectReference = projectNode;
break;
}
}
}
bool changedContent = false;
if (referenceScriptCoreProject)
{
Log.EngineLogger.Info($"Debug mode enabled. Referencing {projectName} in Script-Solution.");
if (projectReference == null)
{
Log.EngineLogger.Warning($"No reference to {projectName} found. Adding...");
projectReference = solution.DocumentElement.AddChild("Project");
changedContent = true;
}
if (projectReference["Path"] != projectFilePath)
{
projectReference["Path"] = projectFilePath;
changedContent = true;
}
}
else if (projectReference != null)
{
Log.EngineLogger.Warning($"Reference to {projectName} found. Removing...");
projectReference.RemoveFromParent(true);
changedContent = true;
}
return changedContent;
}
/// If necessary adds or removes the reference to ScriptCore.dll from the project (.csproj) file. /// If necessary adds or removes the reference to ScriptCore.dll from the project (.csproj) file.
private Result<void> FixupScriptProjectFile(bool referenceScriptCoreProject) private Result<void> FixupScriptProjectFile(bool referenceScriptCoreProject)
{ {
@@ -372,9 +439,20 @@ class Project
} }
} }
bool changedContent = false;
// Try to find a reference to the dll // Try to find a reference to the dll
XmlNode dllReferenceNode = project.DocumentElement.Find("Reference", "Include", "ScriptCore", recursive: true); XmlNode dllReferenceNode = project.DocumentElement.Find("Reference", "Include", "ScriptCore", recursive: true);
mixin SetIfDifferent(XmlNode node, StringView attributeName, StringView wantedValue)
{
if (node.GetAttribute(attributeName)?.Value != wantedValue)
{
node.SetAttribute(attributeName, wantedValue);
changedContent = true;
}
}
if (referenceScriptCoreProject) if (referenceScriptCoreProject)
{ {
Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Project."); Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Project.");
@@ -383,6 +461,7 @@ class Project
{ {
Log.EngineLogger.Warning("Reference to ScriptCore.dll found. Removing..."); Log.EngineLogger.Warning("Reference to ScriptCore.dll found. Removing...");
dllReferenceNode.RemoveFromParent(true); dllReferenceNode.RemoveFromParent(true);
changedContent = true;
} }
if (scriptCoreProjectReference == null) if (scriptCoreProjectReference == null)
@@ -392,16 +471,21 @@ class Project
XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true); XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);
scriptCoreProjectReference = itemGroup.AddChild("ProjectReference"); scriptCoreProjectReference = itemGroup.AddChild("ProjectReference");
changedContent = true;
}
String scriptCoreProjectPath = scope .(); String scriptCoreProjectPath = scope .();
Directory.GetCurrentDirectory(scriptCoreProjectPath); Directory.GetCurrentDirectory(scriptCoreProjectPath);
Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj"); Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj");
Path.ToActualPath(scriptCoreProjectPath); Path.ToActualPath(scriptCoreProjectPath);
if (!Path.Equals(scriptCoreProjectReference.GetAttribute("Include")?.Value, scriptCoreProjectPath))
{
scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath); scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath);
scriptCoreProjectReference.SetAttribute("OutputItemType", "Analyzer"); changedContent = true;
scriptCoreProjectReference.SetAttribute("ReferenceOutputAssembly", "true");
} }
SetIfDifferent!(scriptCoreProjectReference, "OutputItemType", "Analyzer");
SetIfDifferent!(scriptCoreProjectReference, "ReferenceOutputAssembly", "true");
} }
else else
{ {
@@ -409,6 +493,7 @@ class Project
{ {
Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing..."); Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing...");
scriptCoreProjectReference.RemoveFromParent(true); scriptCoreProjectReference.RemoveFromParent(true);
changedContent = true;
} }
if (dllReferenceNode == null) if (dllReferenceNode == null)
@@ -418,16 +503,18 @@ class Project
XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true); XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);
dllReferenceNode = itemGroup.AddChild("Reference"); dllReferenceNode = itemGroup.AddChild("Reference");
changedContent = true;
} }
dllReferenceNode.SetAttribute("Include", "ScriptCore"); SetIfDifferent!(dllReferenceNode, "Include", "ScriptCore");
dllReferenceNode.SetAttribute("OutputItemType", "Analyzer"); SetIfDifferent!(dllReferenceNode, "OutputItemType", "Analyzer");
dllReferenceNode.SetAttribute("ReferenceOutputAssembly", "true"); SetIfDifferent!(dllReferenceNode, "ReferenceOutputAssembly", "true");
XmlNode hintPathNode = dllReferenceNode.Find("HintPath"); XmlNode hintPathNode = dllReferenceNode.Find("HintPath");
if (hintPathNode == null) if (hintPathNode == null)
{ {
hintPathNode = dllReferenceNode.AddChild("HintPath"); hintPathNode = dllReferenceNode.AddChild("HintPath");
changedContent = true;
} }
String pathToScriptCoreDll = scope .(); String pathToScriptCoreDll = scope .();
@@ -441,8 +528,16 @@ class Project
return .Err; return .Err;
} }
if (!Path.Equals(hintPathNode.Text, pathToScriptCoreDll))
{
hintPathNode.SetText(pathToScriptCoreDll); hintPathNode.SetText(pathToScriptCoreDll);
changedContent = true;
} }
}
if (changedContent)
{
Log.EngineLogger.Info("Script project file updated, saving...");
String modfiedProject = new String(projectFileContent.Length * 2); String modfiedProject = new String(projectFileContent.Length * 2);
defer delete modfiedProject; defer delete modfiedProject;
@@ -452,6 +547,11 @@ class Project
{ {
Log.EngineLogger.Error($"Failed to modify {csprojPath}"); Log.EngineLogger.Error($"Failed to modify {csprojPath}");
} }
}
else
{
Log.EngineLogger.Info("Script project file already up to date.");
}
return .Ok; return .Ok;
} }
+10 -2
View File
@@ -375,17 +375,25 @@ class EditorSettings
if (_recentProjects == null) if (_recentProjects == null)
_recentProjects = new List<String>(); _recentProjects = new List<String>();
String absolutePath = new String(value.Length);
Path.GetFullPath(value, absolutePath);
if (absolutePath.EndsWith(Path.DirectorySeparatorChar))
absolutePath.RemoveFromEnd(1);
// Remove duplicates // Remove duplicates
for (var entry in _recentProjects) for (var entry in _recentProjects)
{ {
if (entry == value) if (entry.EndsWith(Path.DirectorySeparatorChar))
entry.RemoveFromEnd(1);
if (Path.Equals(entry, absolutePath))
{ {
delete entry; delete entry;
@entry.Remove(); @entry.Remove();
} }
} }
_recentProjects.Insert(0, new String(value)); _recentProjects.Insert(0, absolutePath);
if (_recentProjects.Count > 10) if (_recentProjects.Count > 10)
{ {
@@ -0,0 +1,21 @@
namespace System.Collections;
extension List<T>
{
public bool Equals(List<T> other)
{
if (this === other)
return true;
if (this.Count != other.Count)
return false;
for (int i < this.Count)
{
if (this[i] != other[i])
return false;
}
return true;
}
}
+14 -5
View File
@@ -29,7 +29,9 @@ static class CoreClrHelper
//static function void(char8* path) LoadScriptAssembly; //static function void(char8* path) LoadScriptAssembly;
static LoadScriptAssemblyFunc _loadScriptAssembly; static LoadScriptAssemblyFunc _loadScriptAssembly;
static function void() _unloadAssemblies; static function void() _unloadScriptAssembly;
static function void() _waitUntilOldAssemblyDead;
// public static unsafe void GetScriptClasses(void** outBuffer, long* length) // public static unsafe void GetScriptClasses(void** outBuffer, long* length)
private function void GetScriptClassesFunc(void** outBuffer, int64* length); private function void GetScriptClassesFunc(void** outBuffer, int64* length);
@@ -108,7 +110,9 @@ static class CoreClrHelper
{ {
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "LoadScriptAssembly", out _loadScriptAssembly); GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "LoadScriptAssembly", out _loadScriptAssembly);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "UnloadAssemblies", out _unloadAssemblies); GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "UnloadScriptAssembly", out _unloadScriptAssembly);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "WaitUntilOldAssemblyDead", out _waitUntilOldAssemblyDead);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "GetScriptClasses", out _getScriptClasses); GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "GetScriptClasses", out _getScriptClasses);
@@ -130,14 +134,19 @@ static class CoreClrHelper
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "InvokeEntityOnCollisionEnter2D", out _entityScriptFunctions.OnCollisionEnter2D); GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "InvokeEntityOnCollisionEnter2D", out _entityScriptFunctions.OnCollisionEnter2D);
} }
public static void LoadAppAssembly(Span<uint8> appAssemblyData, Span<uint8> pdbData) public static void LoadScriptAssembly(Span<uint8> appAssemblyData, Span<uint8> pdbData)
{ {
_loadScriptAssembly(appAssemblyData.Ptr, appAssemblyData.Length, pdbData.Ptr, pdbData.Length); _loadScriptAssembly(appAssemblyData.Ptr, appAssemblyData.Length, pdbData.Ptr, pdbData.Length);
} }
public static void UnloadAssemblies() public static void UnloadScriptAssembly()
{ {
_unloadAssemblies(); _unloadScriptAssembly();
}
public static void WaitUntilOldAssemblyDead()
{
_waitUntilOldAssemblyDead();
} }
/// Gets an array of script class infos. Use FreeScriptClassNames to release the buffer. /// Gets an array of script class infos. Use FreeScriptClassNames to release the buffer.
@@ -16,6 +16,7 @@ class EntitySerializerWrapper : NewScriptClass
function void DeserializeMethod(UUID entityId, void* serializedObjPtr, void* serializerPtr); function void DeserializeMethod(UUID entityId, void* serializedObjPtr, void* serializerPtr);
function void SerializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr); function void SerializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr);
function void DeserializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr); function void DeserializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr);
function void FinishSerializationMethod(void* serializerPtr);
private CreateSerializationContextMethod _createSerializationContextMethod; private CreateSerializationContextMethod _createSerializationContextMethod;
//private ClearSerializationContextMethod _clearSerializationContextMethod; //private ClearSerializationContextMethod _clearSerializationContextMethod;
@@ -25,16 +26,19 @@ class EntitySerializerWrapper : NewScriptClass
private DeserializeMethod _deserializeMethod; private DeserializeMethod _deserializeMethod;
private SerializeStaticMethod _serializeStaticMethod; private SerializeStaticMethod _serializeStaticMethod;
private DeserializeStaticMethod _deserializeStaticMethod; private DeserializeStaticMethod _deserializeStaticMethod;
private FinishSerializationMethod _finishSerializationMethod;
[AllowAppend] [AllowAppend]
public this() : base("GlitchyEngine.Serialization.EntitySerializer", .Empty, .None, false) public this() : base("GlitchyEngine.Serialization.EntitySerializer", .Empty, .None, false)
{ {
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "CreateSerializationContext", out _createSerializationContextMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "CreateSerializationContext", out _createSerializationContextMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "DestroySerializationContext", out _destroySerializationContextMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "DestroySerializationContext", out _destroySerializationContextMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Serialize", out _serializeMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Serialize", out _serializeMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Deserialize", out _deserializeMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Deserialize", out _deserializeMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_SerializeStaticFields", out _serializeStaticMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_SerializeStaticFields", out _serializeStaticMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_DeserializeStaticFields", out _deserializeStaticMethod); CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_DeserializeStaticFields", out _deserializeStaticMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_FinishSerialization", out _finishSerializationMethod);
} }
public void CreateSerializationContext(ScriptInstanceSerializer serializer) public void CreateSerializationContext(ScriptInstanceSerializer serializer)
@@ -93,4 +97,10 @@ class EntitySerializerWrapper : NewScriptClass
_deserializeStaticMethod(@class.FullName.CStr(), serializedObjectPtr, serializerPtr); _deserializeStaticMethod(@class.FullName.CStr(), serializedObjectPtr, serializerPtr);
} }
public void FinishSerialization(ScriptInstanceSerializer serializer)
{
void* serializerPtr = Internal.UnsafeCastToPtr(serializer);
_finishSerializationMethod(serializerPtr);
}
} }
+48 -49
View File
@@ -81,6 +81,8 @@ class EngineClasses
} }
} }
// TODO: Init after FileSystemWatcher so that ScriptEngine's destructor runs before FSW's. The proper fix might be to not delete _userAssemblyWatcher in the static destructor...
[StaticInitAfter(typeof(FileSystemWatcher))]
static class ScriptEngine static class ScriptEngine
{ {
// TODO: Maybe make it possible to set the path to ScriptCore just like for the script project? // TODO: Maybe make it possible to set the path to ScriptCore just like for the script project?
@@ -190,11 +192,11 @@ static class ScriptEngine
} }
/// Changes the path to the current app assembly. /// Changes the path to the current app assembly.
public static void SetAppAssemblyPath(StringView appAssemblyPath) public static void SetAppAssemblyPath(StringView appAssemblyPath, bool retainScriptData)
{ {
_appAssemblyPath.Set(appAssemblyPath); _appAssemblyPath.Set(appAssemblyPath);
ReloadAssemblies(); ReloadAssemblies(retainScriptData);
} }
static void InitRuntime() static void InitRuntime()
@@ -207,32 +209,6 @@ static class ScriptEngine
CoreClrHelper.Init(ScriptCorePath); 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 bool _requestingReload = false;
static void InitAssemblyWatcher() static void InitAssemblyWatcher()
@@ -263,7 +239,7 @@ static class ScriptEngine
Application.Instance.InvokeOnMainThread(new () => Application.Instance.InvokeOnMainThread(new () =>
{ {
ReloadAssemblies(); ReloadAssemblies(retainScriptData: true);
_requestingReload = false; _requestingReload = false;
_userAssemblyWatcher.StartRaisingEvents(); _userAssemblyWatcher.StartRaisingEvents();
@@ -299,23 +275,27 @@ static class ScriptEngine
File.ReadAll(pdbPath, pdbData); File.ReadAll(pdbPath, pdbData);
CoreClrHelper.LoadAppAssembly(data, pdbData); CoreClrHelper.LoadScriptAssembly(data, pdbData);
GetEntitiesFromAssemblies(); GetEntitiesFromAssemblies();
//ScriptGlue.RegisterManagedComponents(); //ScriptGlue.RegisterManagedComponents();
} }
else
{
Log.EngineLogger.Error("Failed to load script assembly, because it doesn't exist.");
}
InitAssemblyWatcher(); InitAssemblyWatcher();
} }
/// Starts the script runtime and sets the context scene. /// Starts the script runtime and sets the context scene.
public static void StartRuntime(Scene context) public static void StartRuntime(Scene context, bool retainScriptData = true)
{ {
Debug.Assert(s_Context == null, "StartRuntime was called twice without StopRuntime in between!"); Debug.Assert(s_Context == null, "StartRuntime was called twice without StopRuntime in between!");
Context = context; Context = context;
ReloadAssemblies(); ReloadAssemblies(retainScriptData);
} }
/// Stopts the script runtime and disposes of all script instances. /// Stopts the script runtime and disposes of all script instances.
@@ -435,18 +415,23 @@ static class ScriptEngine
CoreClrHelper.FreeScriptClassNames(); CoreClrHelper.FreeScriptClassNames();
} }
public static void ReloadAssemblies() public static void ReloadAssemblies(bool retainScriptData = true)
{ {
Debug.Profiler.ProfileFunction!(); Debug.Profiler.ProfileFunction!();
// TODO: Assembly reload might actually take a second. We should consider doing it in a separate thread in the editor.
Log.EngineLogger.Info("Reloading script assemblies."); Log.EngineLogger.Info("Reloading script assemblies.");
ScriptInstanceSerializer contextSerializer = null;
ScriptInstanceSerializer contextSerializer = scope .(); if (retainScriptData)
{
contextSerializer = scope:: .();
contextSerializer.SerializeScriptInstances(); contextSerializer.SerializeScriptInstances();
}
// TODO: Unload script assemblies, remove class handles, fire unload events? UnloadScriptAssemblies();
LoadScriptAssemblies(); LoadScriptAssemblies();
@@ -468,20 +453,36 @@ static class ScriptEngine
} }
} }
if (retainScriptData)
{
contextSerializer.DeserializeScriptInstances(); contextSerializer.DeserializeScriptInstances();
}
Log.EngineLogger.Info("Script assemblies reloaded!"); Log.EngineLogger.Info("Script assemblies reloaded!");
} }
private static void UnloadScriptAssemblies()
{
for (let (id, scriptInstance) in _entityScriptInstances)
{
if (Context.GetEntityByID(id) case .Ok(let entity))
{
ScriptComponent* script = entity.GetComponent<ScriptComponent>();
DestroyInstance(entity, script);
}
else
{
Log.EngineLogger.AssertDebug(false, "Entity script instance exists but no entity?!");
}
}
CoreClrHelper.UnloadScriptAssembly();
CoreClrHelper.WaitUntilOldAssemblyDead();
}
public static void Shutdown() public static void Shutdown()
{ {
/*Mono.mono_domain_set(s_RootDomain, false); UnloadScriptAssemblies();
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. /// Returns the script class with the given name, or null, if no such class exists.
@@ -492,19 +493,17 @@ static class ScriptEngine
return scriptClass; return scriptClass;
} }
internal static void LogScriptException(ScriptException exception, UUID entityId) internal static void LogScriptException(ScriptException exception)
{ {
String entityInfo = scope .(); String entityInfo = scope .();
if (entityId != .Zero && Context != null) if (exception.EntityId != .Zero && Context != null)
{ {
exception.EntityId = entityId; Result<Entity> sourceEntity = Context.GetEntityByID(exception.EntityId);
Result<Entity> sourceEntity = Context.GetEntityByID(entityId);
if (sourceEntity case .Ok(let e)) if (sourceEntity case .Ok(let e))
{ {
entityInfo.AppendF($" ({e.Name} | {entityId})"); entityInfo.AppendF($" ({e.Name} | {exception.EntityId})");
} }
} }
+80 -17
View File
@@ -1,44 +1,107 @@
using System; using System;
using GlitchyEngine.Core; using GlitchyEngine.Core;
using System.Collections;
namespace GlitchyEngine.Scripting; namespace GlitchyEngine.Scripting;
public class ScriptException public class ScriptException
{ {
private String _fullName ~ delete:append _; private String _fullName ~ delete:append _;
private String _message ~ delete:append _; private String _message ~ delete:append _;
private String _stackTrace ~ delete:append _; private StackTrace _stackTrace ~ delete _;
/// The clean stack trace only contains the Managed Stack (the full trace contains one line for the native-to-managed entry)
private StringView _cleanStackTrace;
//TODO
//private ScriptException _innerException ~ _?.ReleaseRef();
public StringView FullName => _fullName; public StringView FullName => _fullName;
public StringView Message => _message; public StringView Message => _message;
public StringView StackTrace => _stackTrace; public StackTrace StackTrace => _stackTrace;
public StringView CleanStackTrace => _cleanStackTrace;
//public ScriptException InnerException => _innerException; public UUID EntityId { get; private set; }
public UUID EntityId { get; set; }
[AllowAppend] [AllowAppend]
public this(UUID entityId, StringView fullExceptionClassName, StringView message, StringView stackTrace) public this(UUID entityId, StringView fullExceptionClassName, StringView message, StackTrace ownStackTrace)
{ {
String allocFullExceptionClassName = append String(fullExceptionClassName); String allocFullExceptionClassName = append String(fullExceptionClassName);
String allocMessage = append String(message); String allocMessage = append String(message);
String allocStackTrace = append String(stackTrace);
EntityId = entityId;
_stackTrace = ownStackTrace;
_fullName = allocFullExceptionClassName; _fullName = allocFullExceptionClassName;
_message = allocMessage; _message = allocMessage;
_stackTrace = allocStackTrace; }
}
// TODO public class StackFrameInfo
_cleanStackTrace = _stackTrace; {
private String _fileName ~ delete:append _;
private String _methodSignature ~ delete:append _;
public int Line;
public int Column;
public StringView FileName => _fileName;
public StringView MethodSignature => _methodSignature;
[AllowAppend]
public this(StringView fileName, StringView methodSignature, int line, int column)
{
String allocFileName = append String(fileName);
String allocMethodSignature = append String(methodSignature);
_fileName = allocFileName;
_methodSignature = allocMethodSignature;
Line = line;
Column = column;
}
public override void ToString(String outBuffer)
{
outBuffer.Append("at ");
if (MethodSignature.IsEmpty)
outBuffer.Append("<unknown>");
else
outBuffer.Append(MethodSignature);
outBuffer.Append(" in ");
if (!_fileName.IsEmpty)
outBuffer.AppendF($"{FileName}, line {Line}:{Column}");
else
outBuffer.Append("<unknown>");
}
}
public class StackTrace
{
private List<StackFrameInfo> _stackFrameInfos ~ DeleteContainerAndItems!(_);
public Span<StackFrameInfo> Frames => _stackFrameInfos;
public this(List<StackFrameInfo> ownStackFrameInfos)
{
_stackFrameInfos = ownStackFrameInfos;
}
[Commutable]
public static bool operator ==(StackTrace a, StackTrace b)
{
if (a == null || b == null)
return a === b;
return a._stackFrameInfos.Equals(b._stackFrameInfos);
}
public override void ToString(String outBuffer)
{
for (let frame in _stackFrameInfos)
{
frame.ToString(outBuffer);
outBuffer.Append('\n');
}
} }
} }
+49 -17
View File
@@ -28,19 +28,29 @@ using internal GlitchyEngine.Content;
// TODO: Move to logger? // TODO: Move to logger?
class MessageOrigin class MessageOrigin
{ {
private UUID _entityId;
private String _fileName ~ delete:append _; private String _fileName ~ delete:append _;
private String _callerMemberName ~ delete:append _;
private int _lineNumber; private int _lineNumber;
private int _columnNumber;
public UUID EntityId => _entityId;
public StringView FileName => _fileName; public StringView FileName => _fileName;
public StringView CallerMemberName => _callerMemberName;
public int LineNumber => _lineNumber; public int LineNumber => _lineNumber;
public int ColumnNumber => _columnNumber;
[AllowAppend] [AllowAppend]
public this(StringView fileName, int lineNumber) public this(UUID entityId, StringView fileName, StringView callerMemberName, int lineNumber, int columnNumber)
{ {
String file = append String(fileName); String file = append String(fileName);
String caller = append String(callerMemberName);
_entityId = entityId;
_fileName = file; _fileName = file;
_callerMemberName = caller;
_lineNumber = lineNumber; _lineNumber = lineNumber;
_columnNumber = columnNumber;
} }
} }
@@ -76,6 +86,7 @@ struct GlueParamAttribute : Attribute
} }
} }
/// Collects all methods in ScriptGlue that have a RegisterCallAttribute. Generates a function pointer for them as well as outputting a json file that can be used to generate the script library side of the ScriptGlue construct.
[AttributeUsage(.Struct)] [AttributeUsage(.Struct)]
struct EngineFunctionsGeneratorAttribute : Attribute, IComptimeTypeApply struct EngineFunctionsGeneratorAttribute : Attribute, IComptimeTypeApply
{ {
@@ -185,11 +196,13 @@ struct EngineFunctionsGeneratorAttribute : Attribute, IComptimeTypeApply
} }
} }
/// A struct containing all pointers to functions that can be called from the script library.
[EngineFunctionsGenerator] [EngineFunctionsGenerator]
struct EngineFunctions struct EngineFunctions
{ {
static Self _functions; static Self _functions;
/// Sends the references to all ScriptGlue functions to the script library.
[RegisterMethod] [RegisterMethod]
public static void FillEngineFunctions() public static void FillEngineFunctions()
{ {
@@ -200,7 +213,7 @@ struct EngineFunctions
} }
} }
/* Adding this attribute to a method will log method entry and returned Result<T> errors */ /// Collects all methods in ScriptGlue that have a RegisterCallAttribute. Generates the assignment of the corresponding function pointer in EngineFunctions.
[AttributeUsage(.Method | .Constructor)] [AttributeUsage(.Method | .Constructor)]
public struct RegisterMethodAttribute : Attribute, IOnMethodInit public struct RegisterMethodAttribute : Attribute, IOnMethodInit
{ {
@@ -252,6 +265,7 @@ static class ScriptGlue
private static String _lastExceptionMessage = new .() ~ delete _; private static String _lastExceptionMessage = new .() ~ delete _;
/// Initializes the glue layer between native engine and script library.
public static void Init() public static void Init()
{ {
if (_setEngineFunctions == null) if (_setEngineFunctions == null)
@@ -264,6 +278,13 @@ static class ScriptGlue
RegisterManagedComponents(); RegisterManagedComponents();
} }
/// Collects and sends the function pointers to all glue functions to the script library.
private static void RegisterCalls()
{
EngineFunctions.FillEngineFunctions();
}
/// Creates the function pointers to create, remove and check for components and sends them to the script library.
public static void RegisterManagedComponents() public static void RegisterManagedComponents()
{ {
Debug.Profiler.ProfileFunction!(); Debug.Profiler.ProfileFunction!();
@@ -278,11 +299,7 @@ static class ScriptGlue
RegisterComponent<MeshRendererComponent>(); RegisterComponent<MeshRendererComponent>();
} }
private static void RegisterCalls() /// Register the function pointers to create, remove and check for components of the specified type and sends them to the script library.
{
EngineFunctions.FillEngineFunctions();
}
private static void RegisterComponent<T>() where T : struct, new private static void RegisterComponent<T>() where T : struct, new
{ {
String fullComponentTypeName = scope String(); String fullComponentTypeName = scope String();
@@ -445,7 +462,7 @@ static class ScriptGlue
#region Log #region Log
[RegisterCall, CallingConvention(.Cdecl)] [RegisterCall, CallingConvention(.Cdecl)]
static void Log_LogMessage(LogLevel logLevel, StringView messagePtr, StringView fileNamePtr, int lineNumber) static void Log_LogMessage(LogLevel logLevel, UUID entityId, StringView messagePtr, StringView fileNamePtr, StringView callerMemberNamePtr, int lineNumber, int columnNumber)
{ {
String escapedMessage = new:ScopedAlloc! String(messagePtr); String escapedMessage = new:ScopedAlloc! String(messagePtr);
@@ -455,9 +472,7 @@ static class ScriptGlue
if (!fileNamePtr.IsEmpty) if (!fileNamePtr.IsEmpty)
{ {
String fileName = new:ScopedAlloc! String(fileNamePtr); MessageOrigin messageOrigin = scope MessageOrigin(entityId, fileNamePtr, callerMemberNamePtr, lineNumber, columnNumber);
MessageOrigin messageOrigin = scope MessageOrigin(fileName, lineNumber);
Log.ClientLogger.Log((LogLevel)logLevel, escapedMessage, messageOrigin); Log.ClientLogger.Log((LogLevel)logLevel, escapedMessage, messageOrigin);
} }
@@ -467,11 +482,27 @@ static class ScriptGlue
} }
} }
[RegisterCall, CallingConvention(.Cdecl)] [Ordered]
static void Log_LogException(UUID entityId, StringView fullExceptionClassName, StringView exceptionMessage, StringView stackTrace) public struct GlueStackFrameInfo
{ {
ScriptException exception = new ScriptException(entityId, fullExceptionClassName, exceptionMessage, stackTrace); public StringView FileName;
ScriptEngine.LogScriptException(exception, entityId); public StringView MethodSignature;
public int32 Line;
public int32 Column;
}
[RegisterCall, CallingConvention(.Cdecl)]
static void Log_LogException(UUID entityId, StringView fullExceptionClassName, StringView exceptionMessage, GlueStackFrameInfo* stackFrameInfos, int32 stackFrameCount)
{
List<StackFrameInfo> frames = new .(stackFrameCount);
for (GlueStackFrameInfo frameInfo in Span<GlueStackFrameInfo>(stackFrameInfos, stackFrameCount))
{
frames.Add(new StackFrameInfo(frameInfo.FileName, frameInfo.MethodSignature, frameInfo.Line, frameInfo.Column));
}
ScriptException exception = new ScriptException(entityId, fullExceptionClassName, exceptionMessage, new StackTrace(frames));
ScriptEngine.LogScriptException(exception);
} }
#endregion #endregion
@@ -642,7 +673,7 @@ static class ScriptGlue
#endregion #endregion
#region TransformComponen #region TransformComponent
[RegisterCall, CallingConvention(.Cdecl)] [RegisterCall, CallingConvention(.Cdecl)]
static EngineResult Transform_GetParent(UUID entityId, out UUID parentId) static EngineResult Transform_GetParent(UUID entityId, out UUID parentId)
@@ -966,7 +997,7 @@ static class ScriptGlue
#endregion Rigidbody2D #endregion Rigidbody2D
#region Camer #region Camera
[RegisterCall, CallingConvention(.Cdecl)] [RegisterCall, CallingConvention(.Cdecl)]
static void Camera_GetProjectionType(UUID entityId, out SceneCamera.ProjectionType projectionType) static void Camera_GetProjectionType(UUID entityId, out SceneCamera.ProjectionType projectionType)
@@ -1534,6 +1565,7 @@ static class ScriptGlue
asset asset
}*/ }*/
/// Set an exception message that the script library can collect when a glue function returned an error.
static void SetExceptionMessage(StringView message) static void SetExceptionMessage(StringView message)
{ {
_lastExceptionMessage.Set(message); _lastExceptionMessage.Set(message);
@@ -1,6 +1,7 @@
using System.Collections; using System.Collections;
using GlitchyEngine.Core; using GlitchyEngine.Core;
using GlitchyEngine.Serialization; using GlitchyEngine.Serialization;
using System;
namespace GlitchyEngine.Scripting; namespace GlitchyEngine.Scripting;
@@ -31,11 +32,22 @@ public class ScriptInstanceSerializer
ScriptEngine.Classes.EntitySerializer.CreateSerializationContext(this); ScriptEngine.Classes.EntitySerializer.CreateSerializationContext(this);
} }
/// Signals, that no more objects will be serialized into this context.
/// This allows the managed serialization context to do cleanup:
/// i. e. for C# we can release references to types which allows us to cleanup the old script assembly.
public void FinishSerialization()
{
ScriptEngine.Classes.EntitySerializer.FinishSerialization(this);
}
/// Serializes all script instances that are currently managed by the ScriptEngine. /// Serializes all script instances that are currently managed by the ScriptEngine.
public void SerializeScriptInstances() public void SerializeScriptInstances()
{ {
Debug.Profiler.ProfileFunction!(); Debug.Profiler.ProfileFunction!();
Log.EngineLogger.Trace($"Serializing {Internal.UnsafeCastToPtr(this)}...");
Init(); Init();
for (let (id, scriptInstance) in ScriptEngine._entityScriptInstances) for (let (id, scriptInstance) in ScriptEngine._entityScriptInstances)
@@ -47,6 +59,8 @@ public class ScriptInstanceSerializer
{ {
SerializeStaticScriptClassFields(scriptClass); SerializeStaticScriptClassFields(scriptClass);
} }
FinishSerialization();
} }
/// Serializes the given script instance. /// Serializes the given script instance.
@@ -68,6 +82,8 @@ public class ScriptInstanceSerializer
{ {
Debug.Profiler.ProfileFunction!(); Debug.Profiler.ProfileFunction!();
Log.EngineLogger.Trace($"Deserializing {Internal.UnsafeCastToPtr(this)}...");
Init(); Init();
for (let (id, script) in ScriptEngine._entityScriptInstances) for (let (id, script) in ScriptEngine._entityScriptInstances)
@@ -79,6 +95,8 @@ public class ScriptInstanceSerializer
{ {
DeserializeStaticScriptClassFields(scriptClass); DeserializeStaticScriptClassFields(scriptClass);
} }
FinishSerialization();
} }
/// Deserializes the data into the given script instance, if there is data available. /// Deserializes the data into the given script instance, if there is data available.
@@ -324,7 +324,7 @@ class SerializedObject
Serialize.Value(writer, ValueView(typeof(StringView), &dataView), environment); Serialize.Value(writer, ValueView(typeof(StringView), &dataView), environment);
case .EngineObjectReference: case .EngineObjectReference:
writer.Type("EngineObject"); writer.Type("EngineObject");
if (field.Data.EngineObject.FullTypeName.IsEmpty) if (!((StringView)field.Data.ListEngineObject.FullTypeName).IsEmpty)
writer.Type((StringView)field.Data.ListEngineObject.FullTypeName); writer.Type((StringView)field.Data.ListEngineObject.FullTypeName);
Serialize.Value(writer, field.Data.EngineObject.ID, environment); Serialize.Value(writer, field.Data.EngineObject.ID, environment);
case .ObjectReference: case .ObjectReference:
+2 -1
View File
@@ -293,7 +293,8 @@ namespace GlitchyEngine.World
private void StartRuntime() private void StartRuntime()
{ {
ScriptEngine.StartRuntime(this); // We don't retain data of script instances, because our callers do that if necessary
ScriptEngine.StartRuntime(this, retainScriptData: false);
// Initialize ScriptComponents // Initialize ScriptComponents
for (let (handle, scriptComponent) in _ecsWorld.Enumerate<ScriptComponent>()) for (let (handle, scriptComponent) in _ecsWorld.Enumerate<ScriptComponent>())
+151 -25
View File
@@ -1,4 +1,11 @@
using System; using GlitchyEngine.Core;
using GlitchyEngine.Extensions;
using GlitchyEngine.Graphics;
using GlitchyEngine.Math;
using GlitchyEngine.Math.Attributes;
using ImGuiNET;
using System;
using System.Buffers;
using System.Collections; using System.Collections;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
@@ -10,12 +17,7 @@ using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using GlitchyEngine.Core; using System.Text.Unicode;
using GlitchyEngine.Extensions;
using GlitchyEngine.Graphics;
using GlitchyEngine.Math;
using GlitchyEngine.Math.Attributes;
using ImGuiNET;
using Component = GlitchyEngine.Core.Component; using Component = GlitchyEngine.Core.Component;
namespace GlitchyEngine.Editor; namespace GlitchyEngine.Editor;
@@ -282,10 +284,10 @@ internal class EntityEditor
if (value is float || value is double || value is Half) if (value is float || value is double || value is Half)
format = "0.#####"; format = "0.#####";
else else
format = "0"; format = null;
} }
if (!format.StartsWith("%")) if (format?.StartsWith("%") == false)
{ {
if (value is float f) if (value is float f)
{ {
@@ -303,14 +305,14 @@ internal class EntityEditor
if (range?.Slider == true) if (range?.Slider == true)
{ {
if (ImGui.SliderScalar(fieldId, dataType, (IntPtr)(&value), (IntPtr)(&min), (IntPtr)(&max), format)) if (ImGui.SliderScalar(fieldId, dataType, (IntPtr)(&value), (IntPtr)(&min), (IntPtr)(&max), format, flags: ImGuiSliderFlags.AlwaysClamp))
{ {
newValue = value; newValue = value;
} }
} }
else else
{ {
if (ImGui.DragScalar(fieldId, dataType, (IntPtr)(&value), dragSpeed, (IntPtr)(&min), (IntPtr)(&max), format)) if (ImGui.DragScalar(fieldId, dataType, (IntPtr)(&value), dragSpeed, (IntPtr)(&min), (IntPtr)(&max), format, flags: ImGuiSliderFlags.AlwaysClamp))
{ {
newValue = value; newValue = value;
} }
@@ -327,9 +329,6 @@ internal class EntityEditor
{ {
unsafe unsafe
{ {
// TODO: Allow escaped unicode chars \u XXXX
// TODO: Add text input validation, so that it isn't possible to type invalid chars
char* value = stackalloc char[4]; char* value = stackalloc char[4];
switch((char)reference!) switch((char)reference!)
{ {
@@ -374,20 +373,143 @@ internal class EntityEditor
// Place string delimiter after encoded UTF8 sequence. // Place string delimiter after encoded UTF8 sequence.
buffer[encodedBytes] = (byte)'\0'; buffer[encodedBytes] = (byte)'\0';
if (ImGui.InputText(fieldId, (IntPtr)buffer, 8)) // Makes sure the data of a char text field is always valid or can be extended to be a valid char.
int CharTextFieldFilter(ImGuiInputTextCallbackData* data)
{ {
if (buffer[0] == '\\' && buffer[1] != '\0') try
{ {
newValue = (char)buffer[1] switch Span<byte> span = new Span<byte>(data->Buf, data->BufTextLen);
if (span.IsEmpty)
return 0;
if (span[0] == '\\')
{ {
'a' => newValue = '\a', // It's just the backslash, so we don't need to do anything yet.
'b' => newValue = '\b', if (span.Length == 1)
'f' => newValue = '\f', return 0;
'n' => newValue = '\n',
'r' => newValue = '\r', if (span[1] == 'a' || span[1] == 'b' || span[1] == 'f' || span[1] == 'n' ||
't' => newValue = '\t', span[1] == 'r' || span[1] == 't' || span[1] == 'v' || span[1] == '\0')
'v' => newValue = '\v', {
_ => newValue = '\\' if (span.Length == 2)
return 0;
// Handle simply ascii escape sequence
if (span[2] != '\0')
{
span[2] = 0;
data->BufTextLen = 2;
data->BufDirty = 1;
}
return 0;
}
else if (span[1] == 'u')
{
// Handle unicode escape sequence \uXXXX
// Check if the next four characters are valid hex digits
for (int i = 2; i < span.Length; i++)
{
char c = (char)span[i];
if (c == '\0')
{
break;
}
if (!Uri.IsHexDigit(c))
{
span[i] = 0;
data->BufTextLen = i;
data->BufDirty = 1;
return 0;
}
}
if (span.Length > 6)
{
span[6] = 0;
data->BufTextLen = 6;
data->BufDirty = 1;
return 0;
}
}
else
{
if (span[1] != '\0')
{
span[1] = 0;
data->BufTextLen = 1;
data->BufDirty = 1;
return 0;
}
return 0;
}
}
else
{
var status = Rune.DecodeFromUtf8(span, out Rune rune, out int bytesConsumed);
// invalid UTF-8 oder Unicode-symbol needs two UTF-16 chars.
if (status != OperationStatus.Done || !rune.IsBmp)
{
span[0] = 0;
data->BufTextLen = 0;
data->BufDirty = 1;
return 0;
}
// TODO: if we want to support Rune as a type, we "just" need to disable this if-check
// Cut off everything after the first C# char (which is the first UTF-16 char of the Rune)
if (bytesConsumed < span.Length)
{
span[bytesConsumed] = 0;
data->BufTextLen = bytesConsumed;
data->BufDirty = 1;
return 0;
}
return 0;
}
}
catch (Exception e)
{
Log.Error($"Failed to process char field {fieldName}: {e.Message}.");
}
return 0;
}
// Make space for the hex code of the char after the textbox
ImGui.SetNextItemWidth(ImGui.CalcItemWidth() / 2);
if (ImGui.InputText(fieldId, (IntPtr)buffer, 8, ImGuiInputTextFlags.CallbackEdit, CharTextFieldFilter))
{
var dataSpan = new ReadOnlySpan<byte>(buffer, 8);
if (dataSpan[0] == '\\' && dataSpan[1] == 'u')
{
// Decode unicode escape sequence \uXXXX
string hex = Encoding.UTF8.GetString(dataSpan[2..]);
if (int.TryParse(hex, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out int codePoint))
{
newValue = (char)codePoint;
}
}
else if (dataSpan[0] == '\\' && dataSpan[1] != '\0')
{
newValue = (char)dataSpan[1] switch
{
'a' => '\a',
'b' => '\b',
'f' => '\f',
'n' => '\n',
'r' => '\r',
't' => '\t',
'v' => '\v',
_ => '\\'
}; };
} }
else else
@@ -398,6 +520,10 @@ internal class EntityEditor
newValue = value[0]; newValue = value[0];
} }
} }
// Show the char as hex code
ImGui.SameLine();
ImGui.TextUnformatted($"(U+{(ushort)(char)reference:X4})");
} }
} }
else if (fieldType == typeof(byte)) else if (fieldType == typeof(byte))
+10 -18
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace GlitchyEngine; namespace GlitchyEngine;
@@ -11,66 +12,57 @@ public static class Input
/// Returns <see langword="true"/> if the specified key is in the pressed down state. /// Returns <see langword="true"/> if the specified key is in the pressed down state.
/// </summary> /// </summary>
/// <param name="key">The key to query.</param> /// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsKeyPressed(Key key) => ScriptGlue.Input_IsKeyPressed(key);
public static extern bool IsKeyPressed(Key key);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the specified <see cref="key"/> is in the released state. /// Returns <see langword="true"/> if the specified <see cref="key"/> is in the released state.
/// </summary> /// </summary>
/// <param name="key">The key to query.</param> /// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsKeyReleased(Key key) => ScriptGlue.Input_IsKeyReleased(key);
public static extern bool IsKeyReleased(Key key);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the state of the specified <see cref="key"/> changed this frame (i.e. changed from pressed to released or from released to pressed). /// Returns <see langword="true"/> if the state of the specified <see cref="key"/> changed this frame (i.e. changed from pressed to released or from released to pressed).
/// </summary> /// </summary>
/// <param name="key">The key to query.</param> /// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsKeyToggled(Key key) => ScriptGlue.Input_IsKeyToggled(key);
public static extern bool IsKeyToggled(Key key);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if <see cref="key"/> is being pressed down this frame /// Returns <see langword="true"/> if <see cref="key"/> is being pressed down this frame
/// (<see cref="IsKeyPressed"/> was <see langword="false"/> last frame and is <see langword="true"/> this frame). /// (<see cref="IsKeyPressed"/> was <see langword="false"/> last frame and is <see langword="true"/> this frame).
/// </summary> /// </summary>
/// <param name="key">The key to query.</param> /// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsKeyPressing(Key key) => ScriptGlue.Input_IsKeyPressing(key);
public static extern bool IsKeyPressing(Key key);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if <see cref="key"/> is being released this frame /// Returns <see langword="true"/> if <see cref="key"/> is being released this frame
/// (<see cref="IsKeyPressed"/> was <see langword="true"/> last frame and is <see langword="false"/> this frame). /// (<see cref="IsKeyPressed"/> was <see langword="true"/> last frame and is <see langword="false"/> this frame).
/// </summary> /// </summary>
/// <param name="key">The key to query.</param> /// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsKeyReleasing(Key key) => ScriptGlue.Input_IsKeyReleasing(key);
public static extern bool IsKeyReleasing(Key key);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the specified mouse button is in the pressed down state. /// Returns <see langword="true"/> if the specified mouse button is in the pressed down state.
/// </summary> /// </summary>
/// <param name="mouseButton">The button to query.</param> /// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsMouseButtonPressed(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonPressed(mouseButton);
public static extern bool IsMouseButtonPressed(MouseButton mouseButton);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the specified mouse button is in the released state. /// Returns <see langword="true"/> if the specified mouse button is in the released state.
/// </summary> /// </summary>
/// <param name="mouseButton">The button to query.</param> /// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsMouseButtonReleased(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonReleased(mouseButton);
public static extern bool IsMouseButtonReleased(MouseButton mouseButton);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the mouse button is being pressed down this frame /// Returns <see langword="true"/> if the mouse button is being pressed down this frame
/// (<see cref="IsMouseButtonPressed"/> was <see langword="false"/> last frame and is <see langword="true"/> this frame). /// (<see cref="IsMouseButtonPressed"/> was <see langword="false"/> last frame and is <see langword="true"/> this frame).
/// </summary> /// </summary>
/// <param name="mouseButton">The button to query.</param> /// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsMouseButtonPressing(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonPressing(mouseButton);
public static extern bool IsMouseButtonPressing(MouseButton mouseButton);
/// <summary> /// <summary>
/// Returns <see langword="true"/> if the mouse button is being released this frame /// Returns <see langword="true"/> if the mouse button is being released this frame
/// (<see cref="IsMouseButtonPressed"/> was <see langword="true"/> last frame and is <see langword="false"/> this frame). /// (<see cref="IsMouseButtonPressed"/> was <see langword="true"/> last frame and is <see langword="false"/> this frame).
/// </summary> /// </summary>
/// <param name="mouseButton">The button to query.</param> /// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)] public static bool IsMouseButtonReleasing(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonReleasing(mouseButton);
public static extern bool IsMouseButtonReleasing(MouseButton mouseButton);
} }
+86 -22
View File
@@ -1,7 +1,11 @@
using GlitchyEngine.Core;
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using GlitchyEngine.Core; using System.Runtime.InteropServices;
using static GlitchyEngine.Log;
namespace GlitchyEngine; namespace GlitchyEngine;
@@ -31,9 +35,10 @@ public class Log
/// <param name="message">The message to log.</param> /// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Trace(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Trace(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Trace, message, callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Trace, ScriptGlue.CurrentEntityId, message, callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -42,9 +47,10 @@ public class Log
/// <param name="message">The message to log.</param> /// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Info(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Info(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Info, message, callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Info, ScriptGlue.CurrentEntityId, message, callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -53,9 +59,10 @@ public class Log
/// <param name="message">The message to log.</param> /// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Warning(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Warning(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Warning, message, callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Warning, ScriptGlue.CurrentEntityId, message, callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -64,9 +71,10 @@ public class Log
/// <param name="message">The message to log.</param> /// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Error(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Error(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Error, message, callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Error, ScriptGlue.CurrentEntityId, message, callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -75,9 +83,10 @@ public class Log
/// <param name="message">The message to log.</param> /// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Critical(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Critical(string message, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Critical, message, callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Critical, ScriptGlue.CurrentEntityId, message, callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -86,9 +95,10 @@ public class Log
/// <param name="obj">The object to serialize.</param> /// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Trace(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Trace(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Trace, obj.ToString(), callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Trace, ScriptGlue.CurrentEntityId, obj.ToString(), callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -97,9 +107,10 @@ public class Log
/// <param name="obj">The object to serialize.</param> /// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Info(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Info(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Info, obj.ToString(), callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Info, ScriptGlue.CurrentEntityId, obj.ToString(), callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -108,9 +119,10 @@ public class Log
/// <param name="obj">The object to serialize.</param> /// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Warning(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Warning(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Warning, obj.ToString(), callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Warning, ScriptGlue.CurrentEntityId, obj.ToString(), callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -119,9 +131,10 @@ public class Log
/// <param name="obj">The object to serialize.</param> /// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Error(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Error(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Error, obj.ToString(), callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Error, ScriptGlue.CurrentEntityId, obj.ToString(), callerFilePath, callerMemberName, callerLineNumber, 0);
} }
/// <summary> /// <summary>
@@ -130,9 +143,30 @@ public class Log
/// <param name="obj">The object to serialize.</param> /// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path of the caller.</param> /// <param name="callerFilePath">File path of the caller.</param>
/// <param name="callerLineNumber">Line number of the caller.</param> /// <param name="callerLineNumber">Line number of the caller.</param>
public static void Critical(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0) /// <param name="callerMemberName">Name of the calling member.</param>
public static void Critical(object obj, [CallerFilePath]string callerFilePath = "", [CallerLineNumber]int callerLineNumber = 0, [CallerMemberName]string callerMemberName = "")
{ {
ScriptGlue.Log_LogMessage(LogLevel.Critical, obj.ToString(), callerFilePath, callerLineNumber); ScriptGlue.Log_LogMessage(LogLevel.Critical, ScriptGlue.CurrentEntityId, obj.ToString(), callerFilePath, callerMemberName, callerLineNumber, 0);
}
[DebuggerDisplay("{ToString(),raw}")]
[StructLayout(LayoutKind.Sequential, Pack = 0)]
[EngineClass("GlitchyEngine.Scripting.ScriptGlue.GlueStackFrameInfo")]
internal struct StackFrameInfo
{
public Native.StringView FileName;
public Native.StringView MethodSignature;
public int LineNumber;
public int ColumnNumber;
}
static string GetFullSignature(MethodBase method)
{
var typeName = method.DeclaringType?.FullName ?? "<unknown>";
var parameters = string.Join(", ",
method.GetParameters().Select(p => $"{p.ParameterType.Name}"));
return $"{typeName}.{method.Name}({parameters})";
} }
/// <summary> /// <summary>
@@ -141,6 +175,36 @@ public class Log
/// <param name="exception">The exception to log.</param> /// <param name="exception">The exception to log.</param>
public static void Exception(Exception exception) public static void Exception(Exception exception)
{ {
ScriptGlue.Log_LogException(UUID.Zero, exception.GetType().FullName, exception.Message, exception.StackTrace); StackTrace st = new StackTrace(exception, true);
StackFrame[] frames = st.GetFrames();
StackFrameInfo[] stackFrameInfos = new StackFrameInfo[frames.Length];
// We only need StackFrameInfo for exception logging, so it's okay to manually handle the marshalling here.
foreach (StackFrame stackFrame in frames)
{
MethodBase? method = stackFrame.GetMethod();
string methodSignature = method != null ? GetFullSignature(method) : "<unknown>";
stackFrameInfos[frames.IndexOf(stackFrame)] = new StackFrameInfo
{
FileName = Native.StringView.FromManagedString(stackFrame.GetFileName()),
MethodSignature = Native.StringView.FromManagedString(methodSignature),
LineNumber = stackFrame.GetFileLineNumber(),
ColumnNumber = stackFrame.GetFileColumnNumber()
};
}
unsafe
{
fixed (StackFrameInfo* stackFrames = stackFrameInfos)
{
ScriptGlue.Log_LogException(ScriptGlue.CurrentEntityId, exception.GetType().FullName, exception.Message, stackFrames, stackFrameInfos.Length);
}
}
foreach (StackFrameInfo stackFrameInfo in stackFrameInfos)
{
Native.StringView.FreeNativeMemory(stackFrameInfo.FileName);
}
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<BaseOutputPath></BaseOutputPath> <BaseOutputPath></BaseOutputPath>
<RootNamespace>GlitchyEngine</RootNamespace> <RootNamespace>GlitchyEngine</RootNamespace>
+132 -6
View File
@@ -8,12 +8,15 @@ using ImGuiNET;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Loader; using System.Runtime.Loader;
using System.Text; using System.Text;
using System.Threading;
using System.Xml.Linq;
namespace GlitchyEngine; namespace GlitchyEngine;
@@ -111,25 +114,53 @@ internal static unsafe partial class ScriptGlue
} }
} }
/// <summary>
/// Counter for the number of script contexts that have been created. This allows us to debug and track the number of script contexts that have been created and destroyed.
/// </summary>
private static int _contextCounter = 0;
private static void LoadAssembly(Stream assemblyStream, Stream? pdbStream) private static void LoadAssembly(Stream assemblyStream, Stream? pdbStream)
{ {
_scriptAssemblyContext ??= new AssemblyLoadContext("ScriptContext", true); Debug.Assert(_scriptAssemblyContext == null);
Debug.Assert(_appAssembly == null);
// A new AssemblyLoadContext for the script assembly allows us to unload the assembly later.
_scriptAssemblyContext = new AssemblyLoadContext($"ScriptContext_{Interlocked.Increment(ref _contextCounter)}", true);
_appAssembly = _scriptAssemblyContext.LoadFromStream(assemblyStream, pdbStream); _appAssembly = _scriptAssemblyContext.LoadFromStream(assemblyStream, pdbStream);
Debug.Assert(_appAssembly != null); Debug.Assert(_appAssembly != null);
} }
private static WeakReference? _weakAssemblyRef;
private static string? _lastAssemblyName;
/// <summary> /// <summary>
/// Called by the engine to unload the assembly (containing user scripts). /// Called by the engine to unload the assembly containing user scripts.
/// </summary> /// </summary>
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
public static void UnloadAssemblies() public static void UnloadScriptAssembly()
{ {
try try
{ {
_scriptAssemblyContext?.Unload(); Debug.Assert(EntityScriptInstances.Count == 0, "Can not unload assemblies while entities are still active!");
// TODO: Should we allow calling unload when we never loaded?
if (_scriptAssemblyContext == null)
return;
_weakAssemblyRef = new WeakReference(_scriptAssemblyContext);
_lastAssemblyName = _scriptAssemblyContext.Name;
Debug.WriteLine($"--- Killing {_lastAssemblyName}...");
// TODO: we should probably have a way to collect and terminate threads. (like a EngineThread override or something)
_scriptAssemblyContext.Unload();
_scriptAssemblyContext = null; _scriptAssemblyContext = null;
_appAssembly = null;
GC.Collect();
GC.WaitForPendingFinalizers();
} }
catch (Exception e) catch (Exception e)
{ {
@@ -137,6 +168,42 @@ internal static unsafe partial class ScriptGlue
} }
} }
/// <summary>
/// Waits until the old assembly is dead.
/// </summary>
[UnmanagedCallersOnly]
public static void WaitUntilOldAssemblyDead()
{
try
{
if (_weakAssemblyRef == null)
return;
int iterations = 0;
while (_weakAssemblyRef.IsAlive)
{
GC.Collect();
GC.WaitForPendingFinalizers();
// We must sleep a bit, otherwise we absolutely wreck the debugger.
Thread.Sleep(10);
iterations++;
}
Debug.WriteLine($"--- {_lastAssemblyName} is now dead after {iterations} garbage collections!");
_weakAssemblyRef = null;
_lastAssemblyName = null;
}
catch (Exception e)
{
Log.Exception(e);
}
}
/// <summary>
/// Waits until the old assembly is dead.
/// </summary>
private static NativeScriptClassInfo[]? _unsafeClasses; private static NativeScriptClassInfo[]? _unsafeClasses;
[StructLayout(LayoutKind.Sequential, Pack = 0)] [StructLayout(LayoutKind.Sequential, Pack = 0)]
@@ -272,11 +339,16 @@ internal static unsafe partial class ScriptGlue
_unsafeClasses = null; _unsafeClasses = null;
} }
private static readonly AsyncLocal<UUID> _currentEntityId = new ();
internal static UUID CurrentEntityId => _currentEntityId.Value;
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
public static void ShowEntityEditor(UUID entityId) public static void ShowEntityEditor(UUID entityId)
{ {
try try
{ {
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId]; (Entity entity, Type type) = EntityScriptInstances[entityId];
EntityEditor.ShowEntityEditor(entity); EntityEditor.ShowEntityEditor(entity);
} }
@@ -284,6 +356,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
@@ -291,6 +367,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId]; (Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnCreate(); entity.OnCreate();
} }
@@ -298,6 +375,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@@ -305,6 +386,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId]; (Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnUpdate(deltaTime); entity.OnUpdate(deltaTime);
} }
@@ -312,6 +394,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@@ -319,6 +405,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId]; (Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnCollisionEnter2D(collision); entity.OnCollisionEnter2D(collision);
} }
@@ -326,6 +413,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
@@ -333,6 +424,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
if (EntityScriptInstances.Remove(entityId, out var match) && callDestroy > 0) if (EntityScriptInstances.Remove(entityId, out var match) && callDestroy > 0)
{ {
match.Entity.OnDestroy(); match.Entity.OnDestroy();
@@ -342,6 +434,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
internal static Type? GetTypeFromNativeString(byte* fullTypeName) internal static Type? GetTypeFromNativeString(byte* fullTypeName)
@@ -365,6 +461,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
Type? scriptType = GetTypeFromNativeString(scriptClassName); Type? scriptType = GetTypeFromNativeString(scriptClassName);
Debug.Assert(scriptType != null, "Script class Type not found."); Debug.Assert(scriptType != null, "Script class Type not found.");
@@ -379,6 +476,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
struct ComponentFunctionPointers struct ComponentFunctionPointers
@@ -459,6 +560,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
if (!EntityScriptInstances.TryGetValue(entityId, out var match)) if (!EntityScriptInstances.TryGetValue(entityId, out var match))
return; return;
@@ -468,6 +570,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
@@ -475,6 +581,7 @@ internal static unsafe partial class ScriptGlue
{ {
try try
{ {
_currentEntityId.Value = entityId;
if (!EntityScriptInstances.TryGetValue(entityId, out var match)) if (!EntityScriptInstances.TryGetValue(entityId, out var match))
return; return;
@@ -484,6 +591,10 @@ internal static unsafe partial class ScriptGlue
{ {
Log.Exception(e); Log.Exception(e);
} }
finally
{
_currentEntityId.Value = UUID.Zero;
}
} }
[UnmanagedCallersOnly] [UnmanagedCallersOnly]
@@ -518,6 +629,19 @@ internal static unsafe partial class ScriptGlue
} }
} }
[UnmanagedCallersOnly]
public static void EntitySerializer_FinishSerialization(IntPtr engineSerializer)
{
try
{
EntitySerializer.FinishSerialization(engineSerializer);
}
catch (Exception e)
{
Log.Exception(e);
}
}
#endregion EntitySerializer #endregion EntitySerializer
#endregion #endregion
@@ -544,14 +668,16 @@ internal static unsafe partial class ScriptGlue
public static void Entity_GetScriptInstance(UUID entityId, out Entity? instance) public static void Entity_GetScriptInstance(UUID entityId, out Entity? instance)
{ {
// We currently can implement this method here, because we only have C# scripts. // 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 we ever need to do something to interop with other script languages, then this would probably need to communicate with the engine.
if (EntityScriptInstances.TryGetValue(entityId, out var match)) if (EntityScriptInstances.TryGetValue(entityId, out var match))
{ {
instance = match.Entity; instance = match.Entity;
} }
else
{
instance = null; instance = null;
} }
}
public static void Serialization_SerializeField(IntPtr serializationContext, SerializationType type, string fieldName, object? valueObject, string? fullTypeName) public static void Serialization_SerializeField(IntPtr serializationContext, SerializationType type, string fieldName, object? valueObject, string? fullTypeName)
{ {
+13 -2
View File
@@ -33,8 +33,19 @@ internal static class EntitySerializer
internal static void DestroySerializationContext(IntPtr engineSerializer) internal static void DestroySerializationContext(IntPtr engineSerializer)
{ {
SerializationObjects.Remove(engineSerializer); if (SerializationObjects.Remove(engineSerializer, out var serializedObjects))
DeserializationObjects.Remove(engineSerializer); {
serializedObjects.Clear();
}
if (DeserializationObjects.Remove(engineSerializer, out var deserializationObjects))
{
deserializationObjects.Clear();
}
}
internal static void FinishSerialization(IntPtr engineSerializer)
{
ClearSerializationContext(engineSerializer);
} }
internal static void Serialize(Entity entity, IntPtr engineObject, IntPtr engineSerializer) internal static void Serialize(Entity entity, IntPtr engineObject, IntPtr engineSerializer)
+2 -1
View File
@@ -107,6 +107,7 @@ public class SerializedObject
public void Serialize(Entity entity) public void Serialize(Entity entity)
{ {
_serializedClasses.Add(entity, this);
SerializeInstanceFields(entity); SerializeInstanceFields(entity);
} }
@@ -321,7 +322,7 @@ public class SerializedObject
{ {
if (fieldType.IsSubclassOf(typeof(EngineObject))) if (fieldType.IsSubclassOf(typeof(EngineObject)))
{ {
AddValueTypeField(fieldName, SerializationType.ObjectReference, ((EngineObject?)fieldValue)?.UUID ?? UUID.Zero, fieldValue?.GetType().FullName); AddValueTypeField(fieldName, SerializationType.EngineObjectReference, ((EngineObject?)fieldValue)?.UUID ?? UUID.Zero, fieldValue?.GetType().FullName);
} }
else else
{ {
+25 -2
View File
@@ -122,7 +122,8 @@ public class ScriptGlueGenerator : IIncrementalGenerator
None, None,
Ref, Ref,
In, In,
Out Out,
Pointer
} }
private static (MappedType Type, TypeModifier Modifier) DecodeType(string beefType, Dictionary<string, MappedType> beefTypeToMappedType) private static (MappedType Type, TypeModifier Modifier) DecodeType(string beefType, Dictionary<string, MappedType> beefTypeToMappedType)
@@ -145,6 +146,15 @@ public class ScriptGlueGenerator : IIncrementalGenerator
beefType = beefType.Substring(3); beefType = beefType.Substring(3);
} }
if (beefTypeToMappedType.TryGetValue(beefType, out var mapping))
return (mapping, modifier);
if (beefType.EndsWith("*"))
{
modifier = TypeModifier.Pointer;
beefType = beefType.Substring(0, beefType.Length - 1);
}
if (beefTypeToMappedType.TryGetValue(beefType, out MappedType? mappedType)) if (beefTypeToMappedType.TryGetValue(beefType, out MappedType? mappedType))
{ {
return (mappedType, modifier); return (mappedType, modifier);
@@ -296,6 +306,14 @@ public class ScriptGlueGenerator : IIncrementalGenerator
} }
parameterText.Append(parameterType.CSharpTypeName); parameterText.Append(parameterType.CSharpTypeName);
switch (parameterModifier)
{
case TypeModifier.Pointer:
parameterText.Append("*");
break;
}
parameterText.Append(", "); parameterText.Append(", ");
} }
@@ -345,7 +363,12 @@ public class ScriptGlueGenerator : IIncrementalGenerator
break; break;
} }
parameterText.Append($"{parameterType.CSharpWrapperType} {param.Name}"); parameterText.Append($"{parameterType.CSharpWrapperType}");
if (parameterModifier == TypeModifier.Pointer)
parameterText.Append("*");
parameterText.Append($" {param.Name}");
switch (parameterModifier) switch (parameterModifier)
{ {