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]
PostBuildCmds = ["CopyFilesIfNewer(\"$(WorkspaceDir)/bin/vswhere.exe\", \"$(TargetDir)\")"]
DebugCommandArguments = "\"D:\\Development\\Git\\SingleStateToOrbit\""
DebugCommandArguments = "\"$(ProjectDir)/../GlitchyEngine.Test/content/TestProject\""
PreprocessorMacros = ["DEBUG", "IMGUI"]
[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.
class DteOpenRequest
{
private const int ColdStartMaxPolls = 120;
private const int ColdStartPollIntervalMs = 500;
private append String _solutionPath = .();
private append String _devenvPath = .();
private append String _fileName = .();
@@ -40,6 +37,7 @@ class DteOpenRequest
}
public int LineNumber;
public int ColumnNumber;
private static int32 sWorkerActive = 0;
@@ -83,7 +81,7 @@ class DteOpenRequest
if (FileName.IsEmpty)
VisualStudioDte.ActivateMainWindow(dte);
else
VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber).IgnoreError();
VisualStudioDte.OpenFileAtLine(dte, FileName, LineNumber, ColumnNumber).IgnoreError();
delete dte;
@@ -107,13 +105,13 @@ class DteOpenRequest
// Wait for the new instance to finish loading the solution (Solution.FullName stays empty
// 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))
{
VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber).IgnoreError();
VisualStudioDte.OpenFileAtLine(startedDte, FileName, LineNumber, ColumnNumber).IgnoreError();
delete startedDte;
return;
@@ -14,6 +14,9 @@ namespace GlitchyEditor.CodeEditors;
/// All methods must be called from a thread that is initialized for COM (CoInitializeEx).
static class VisualStudioDte
{
public const int MaxPolls = 120;
public const int PollIntervalMs = 500;
private const String MonikerPrefix = "!VisualStudio.DTE";
/// EnvDTE.Constants.vsViewKindCode
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
/// 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"));
defer delete itemOperations;
@@ -125,7 +128,7 @@ static class VisualStudioDte
}
if (lineNumber > 0)
GoToLine(dte, lineNumber);
GoToLine(dte, lineNumber, columnNumber);
ActivateMainWindow(dte);
@@ -198,18 +201,18 @@ static class VisualStudioDte
return path;
}
private static void GoToLine(ComDispatch dte, int lineNumber)
private static void GoToLine(ComDispatch dte, int lineNumber, int columnNumber)
{
ComDispatch document = null;
defer { delete document; }
// 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))
break;
Thread.Sleep(100);
Thread.Sleep(PollIntervalMs);
}
if (document == null)
@@ -224,9 +227,14 @@ static class VisualStudioDte
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))
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);
delete selection;
+7 -1
View File
@@ -1,9 +1,15 @@
using System;
namespace GlitchyEditor.CodeEditors;
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();
}
@@ -21,14 +21,15 @@ class RiderIdeAdapter : IIdeAdapter
_ideInstallation = ideInstallation;
}
public void OpenScript(StringView fileName, int lineNumber)
public void OpenScript(StringView fileName, int lineNumber, int columnNumber)
{
String solutionPath = scope .();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
ProcessStartInfo startInfo = scope .();
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);
}
@@ -18,17 +18,17 @@ class VisualStudioIdeAdapter : IIdeAdapter
_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()
{
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();
Editor.Instance.CurrentProject.GetPathToScriptSolutionFile(solutionPath);
@@ -57,6 +57,7 @@ class VisualStudioIdeAdapter : IIdeAdapter
request.DevenvPath = _ideInstallation.Path;
request.FileName = fileName;
request.LineNumber = lineNumber;
request.ColumnNumber = columnNumber;
DteOpenRequest.Start(request);
#else
@@ -66,6 +66,8 @@ namespace GlitchyEditor.EditWindows
{
_entityToHighlight = e;
SelectEntity(e, true);
Entity walker = _entityToHighlight;
while (walker.Parent != null)
+32 -13
View File
@@ -8,6 +8,7 @@ using GlitchyEngine.Scripting;
using GlitchyEngine.Renderer;
using GlitchyEngine;
using GlitchyEditor.CodeEditors;
using System.IO;
namespace GlitchyEditor.EditWindows;
@@ -295,12 +296,30 @@ class LogWindow : EditorWindow
{
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)
ImGui.TextUnformatted(message.Source.Exception.StackTrace);
else
ImGui.TextUnformatted(message.Source.Exception.CleanStackTrace);
relativeSourcePath.Replace(Editor.Instance.ContentManager.ResourcesDirectory, "Resources");
relativeSourcePath.Replace(Editor.Instance.ContentManager.AssetDirectory, "Assets");
//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();
}
@@ -319,6 +338,13 @@ class LogWindow : EditorWindow
{
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.
@@ -373,15 +399,8 @@ class LogWindow : EditorWindow
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);
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?
LogMessage logMessage = new LogMessage(timestamp, message, .Error, new MessageSource(){Entity = exception.EntityId, Exception = exception, IsEngineMessage = false});
+61 -62
View File
@@ -75,14 +75,6 @@ namespace GlitchyEditor
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 _;
/// Gets or sets the path of the current scene.
@@ -720,6 +712,8 @@ namespace GlitchyEditor
private void CloseCurrentProject()
{
Log.EngineLogger.Trace("Closing current Project...");
CloseCurrentScene();
SetEditorScene(null);
@@ -729,6 +723,8 @@ namespace GlitchyEditor
// TODO: Do actual work here!
delete _currentProject;
_currentProject = null;
Log.EngineLogger.Trace("Project closed.");
}
private Result<void> LoadAndOpenProject(StringView workspacePath)
@@ -752,29 +748,43 @@ namespace GlitchyEditor
CloseCurrentProject();
Log.EngineLogger.Trace("Opening Project...");
_currentProject = project;
_editor.CurrentProject = _currentProject;
Log.EngineLogger.Trace("Preparing asset manager...");
String appAssemblyPath = scope String();
_contentManager.SetAssetCacheDirectory(_currentProject.GetScopedPath!(".cache"));
_contentManager.SetAssetDirectory(_currentProject.AssetsFolder);
_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)
{
String lastSceneFile = _currentProject.GetScopedPath!(_currentProject.UserSettings.LastOpenedScene);
if (File.Exists(lastSceneFile))
{
Log.EngineLogger.Trace("Opening last opened scene...");
LoadSceneFile(lastSceneFile);
}
else
{
Log.EngineLogger.Trace("Opening new scene...");
CreateAndOpenNewScene();
}
}
else
{
Log.EngineLogger.Trace("Opening new scene...");
CreateAndOpenNewScene();
}
UpdateWindowTitle();
@@ -784,6 +794,8 @@ namespace GlitchyEditor
EditorApp.Instance.Settings.Save();
}
Log.EngineLogger.Trace("Project opened.");
return .Ok;
}
@@ -808,27 +820,34 @@ namespace GlitchyEditor
append ScriptInstanceSerializer _scriptSerializer = .();
/// Starts the play mode for the current scene
private void OnScenePlay()
private void OnScenePlay(SceneState newSceneState)
{
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.");
if (EditorApp.Instance.Settings.EditorSettings.ClearLogOnPlay)
Editor.Instance.LogWindow.ClearLog();
/// Save the in memory state of the script intances so se can recreate it after stopping play/simulation
_scriptSerializer.SerializeScriptInstances();
_editor.SceneViewportWindow.EditorMode = false;
_sceneState = .Play;
_sceneState = newSceneState;
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);
_scriptSerializer.DeserializeScriptInstances();
if (_sceneState == .Play)
{
_scriptSerializer.DeserializeScriptInstances();
}
}
_editor.CurrentScene = _activeScene;
@@ -837,34 +856,25 @@ namespace GlitchyEditor
SwitchToPlayWindow();
}
enum PlayMode
{
Play,
Editor,
Simulation
}
/// Activates the given scene.
/// @param scene The scene to be activated.
/// @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.
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();
if (newPlayMode != null)
switch (newSceneState)
{
switch (newPlayMode.Value)
{
case .Play:
ScriptEngine.ApplicationInfo.IsInPlayMode = true;
case .Editor:
ScriptEngine.ApplicationInfo.IsInEditMode = true;
case .Simulation:
// No scripts in Simulation
ScriptEngine.ApplicationInfo.IsInEditMode = false;
ScriptEngine.ApplicationInfo.IsInPlayMode = false;
}
case .Play:
ScriptEngine.ApplicationInfo.IsInPlayMode = true;
case .Edit:
ScriptEngine.ApplicationInfo.IsInEditMode = true;
case .Simulate:
// No scripts in Simulation
ScriptEngine.ApplicationInfo.IsInEditMode = false;
ScriptEngine.ApplicationInfo.IsInPlayMode = false;
}
if (scene != null)
@@ -875,25 +885,6 @@ namespace GlitchyEditor
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
private void OnScenePause()
{
@@ -924,13 +915,13 @@ namespace GlitchyEditor
if (_activeScene == null)
return;
SetActiveScene(_editorScene, startRuntime: true, startSimulation: false, newPlayMode: .Editor);
// TODO: _editorScene.DeserializeScripts(scriptData);
Log.EngineLogger.Trace("Stopping current scene and returing to edit mode...");
_editor.SceneViewportWindow.EditorMode = true;
_sceneState = .Edit;
SetActiveScene(_editorScene, startRuntime: true, startSimulation: false, _sceneState);
_editor.CurrentScene = _activeScene;
_isPaused = false;
@@ -943,14 +934,19 @@ namespace GlitchyEditor
* into "Editor"-mode (because Game-Mode works on a copy of the scene).
*/
GameViewportSizeChanged(null, _editor.GameViewportWindow.RenderedViewportSize);
if (_scriptSerializer.SerializedObjectCount != 0)
{
// Reconstruct state before play
_scriptSerializer.DeserializeScriptInstances();
_scriptSerializer.Clear();
}
}
if (EditorApp.Instance.Settings.EditorSettings.SwitchToEditorOnStop)
SwitchToEditorWindow();
// Reconstruct state before play
_scriptSerializer.DeserializeScriptInstances();
_scriptSerializer.Clear();
Log.EngineLogger.Trace("Switched to edit mode.");
}
/// Stops the scene and cleans up the subsystems to allow loading another scene.
@@ -983,6 +979,8 @@ namespace GlitchyEditor
/// Sets the current editor scene
private void SetEditorScene(Scene scene, ScriptInstanceSerializer serializedObjects = null)
{
Log.EngineLogger.Trace($"Switching to scene \"{(scene?.Name ?? "<none>")}\"...");
_editorScene?.Stop();
SetReference!(_editorScene, scene);
@@ -1002,6 +1000,7 @@ namespace GlitchyEditor
}
UpdateWindowTitle();
Log.EngineLogger.Trace($"Switched scene to \"{(scene?.Name ?? "<none>")}\".");
}
/// Creates a new default scene.
@@ -1239,7 +1238,7 @@ namespace GlitchyEditor
ImGui.PushID(0);
if (ImGui.ImageButton("", _editorIcons.Play, .(size, size), .Zero, .Ones))
OnScenePlay();
OnScenePlay(.Play);
ImGui.PopID();
@@ -1250,7 +1249,7 @@ namespace GlitchyEditor
ImGui.PushID(1);
if (ImGui.ImageButton("", _editorIcons.Simulate, .(size, size), .Zero, .Ones))
OnSceneSimulate();
OnScenePlay(.Simulate);
ImGui.PopID();
@@ -1522,7 +1521,7 @@ namespace GlitchyEditor
if(ImGui.BeginMenu("Tools", true))
{
if(ImGui.MenuItem("Reload Scripts"))
ScriptEngine.ReloadAssemblies();
ScriptEngine.ReloadAssemblies(retainScriptData: true);
#if GE_EDITOR_IMGUI_DEMO
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)
{
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
{
+145 -45
View File
@@ -36,7 +36,10 @@ class Project
[AllowAppend]
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();
PathInProject(_assetsFolder, "Assets");
@@ -80,6 +83,27 @@ class Project
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.
private void InitUserSettings()
{
@@ -274,7 +298,8 @@ class 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)
{
String solutionPath = scope .();
@@ -293,50 +318,92 @@ class Project
solution.LoadFromString(solutionFileContent);
XmlNode scriptCoreProjectReference = null;
bool changedContent = false;
String scriptCoreFilePath = scope .();
GetScriptCoreProjectFilePath(scriptCoreFilePath);
changedContent |= Try!(AddProjectToSolution(solution, scriptCoreFilePath, referenceScriptCoreProject));
String scriptCoreGeneratorFilePath = scope .();
GetScriptCoreGeneratorProjectFilePath(scriptCoreGeneratorFilePath);
changedContent |= Try!(AddProjectToSolution(solution, scriptCoreGeneratorFilePath, referenceScriptCoreProject));
String scriptCoreGeneratorTestFilePath = scope .();
GetScriptCoreGeneratorTestProjectFilePath(scriptCoreGeneratorTestFilePath);
changedContent |= Try!(AddProjectToSolution(solution, scriptCoreGeneratorTestFilePath, referenceScriptCoreProject));
String imGuiDotNetFilePath = scope .();
GetImGuiDotNetProjectFilePath(imGuiDotNetFilePath);
changedContent |= Try!(AddProjectToSolution(solution, imGuiDotNetFilePath, referenceScriptCoreProject));
if (changedContent)
{
Log.EngineLogger.Info("Script solution file updated, saving...");
String modifiedSolution = new String(solutionFileContent.Length * 2);
defer delete modifiedSolution;
solution.SaveToString(modifiedSolution);
if (File.WriteAllText(solutionPath, modifiedSolution) case .Err)
{
Log.EngineLogger.Error($"Failed to modify {solutionPath}");
return .Err;
}
}
else
{
Log.EngineLogger.Info("Script solution file already up to date.");
}
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("ScriptCore.csproj", true))
if (includeAttribute.Value.Contains(projectName, true))
{
scriptCoreProjectReference = projectNode;
projectReference = projectNode;
break;
}
}
}
bool changedContent = false;
if (referenceScriptCoreProject)
{
Log.EngineLogger.Info("Debug mode enabled. Referencing ScriptCore.csproj in Script-Solution.");
Log.EngineLogger.Info($"Debug mode enabled. Referencing {projectName} in Script-Solution.");
if (scriptCoreProjectReference == null)
if (projectReference == null)
{
Log.EngineLogger.Warning("No reference to ScriptCore.csproj found. Adding...");
scriptCoreProjectReference = solution.DocumentElement.AddChild("Project");
Log.EngineLogger.Warning($"No reference to {projectName} found. Adding...");
projectReference = solution.DocumentElement.AddChild("Project");
changedContent = true;
}
String projectFilePath = scope .();
GetScriptCoreProjectFilePath(projectFilePath);
scriptCoreProjectReference["Path"] = projectFilePath;
if (projectReference["Path"] != projectFilePath)
{
projectReference["Path"] = projectFilePath;
changedContent = true;
}
}
else if (scriptCoreProjectReference != null)
else if (projectReference != null)
{
Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing...");
scriptCoreProjectReference.RemoveFromParent(true);
Log.EngineLogger.Warning($"Reference to {projectName} found. Removing...");
projectReference.RemoveFromParent(true);
changedContent = true;
}
String modifiedSolution = new String(solutionFileContent.Length * 2);
defer delete modifiedSolution;
solution.SaveToString(modifiedSolution);
if (File.WriteAllText(solutionPath, modifiedSolution) case .Err)
{
Log.EngineLogger.Error($"Failed to modify {solutionPath}");
return .Err;
}
return .Ok;
return changedContent;
}
/// If necessary adds or removes the reference to ScriptCore.dll from the project (.csproj) file.
@@ -372,9 +439,20 @@ class Project
}
}
bool changedContent = false;
// Try to find a reference to the dll
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)
{
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...");
dllReferenceNode.RemoveFromParent(true);
changedContent = true;
}
if (scriptCoreProjectReference == null)
@@ -392,16 +471,21 @@ class Project
XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);
scriptCoreProjectReference = itemGroup.AddChild("ProjectReference");
String scriptCoreProjectPath = scope .();
Directory.GetCurrentDirectory(scriptCoreProjectPath);
Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj");
Path.ToActualPath(scriptCoreProjectPath);
scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath);
scriptCoreProjectReference.SetAttribute("OutputItemType", "Analyzer");
scriptCoreProjectReference.SetAttribute("ReferenceOutputAssembly", "true");
changedContent = true;
}
String scriptCoreProjectPath = scope .();
Directory.GetCurrentDirectory(scriptCoreProjectPath);
Path.Combine(scriptCoreProjectPath, "../ScriptCore/ScriptCore.csproj");
Path.ToActualPath(scriptCoreProjectPath);
if (!Path.Equals(scriptCoreProjectReference.GetAttribute("Include")?.Value, scriptCoreProjectPath))
{
scriptCoreProjectReference.SetAttribute("Include", scriptCoreProjectPath);
changedContent = true;
}
SetIfDifferent!(scriptCoreProjectReference, "OutputItemType", "Analyzer");
SetIfDifferent!(scriptCoreProjectReference, "ReferenceOutputAssembly", "true");
}
else
{
@@ -409,6 +493,7 @@ class Project
{
Log.EngineLogger.Warning("Reference to ScriptCore.csproj found. Removing...");
scriptCoreProjectReference.RemoveFromParent(true);
changedContent = true;
}
if (dllReferenceNode == null)
@@ -418,16 +503,18 @@ class Project
XmlNode itemGroup = project.DocumentElement.Find("ItemGroup", recursive: true);
dllReferenceNode = itemGroup.AddChild("Reference");
changedContent = true;
}
dllReferenceNode.SetAttribute("Include", "ScriptCore");
dllReferenceNode.SetAttribute("OutputItemType", "Analyzer");
dllReferenceNode.SetAttribute("ReferenceOutputAssembly", "true");
SetIfDifferent!(dllReferenceNode, "Include", "ScriptCore");
SetIfDifferent!(dllReferenceNode, "OutputItemType", "Analyzer");
SetIfDifferent!(dllReferenceNode, "ReferenceOutputAssembly", "true");
XmlNode hintPathNode = dllReferenceNode.Find("HintPath");
if (hintPathNode == null)
{
hintPathNode = dllReferenceNode.AddChild("HintPath");
changedContent = true;
}
String pathToScriptCoreDll = scope .();
@@ -441,16 +528,29 @@ class Project
return .Err;
}
hintPathNode.SetText(pathToScriptCoreDll);
if (!Path.Equals(hintPathNode.Text, pathToScriptCoreDll))
{
hintPathNode.SetText(pathToScriptCoreDll);
changedContent = true;
}
}
String modfiedProject = new String(projectFileContent.Length * 2);
defer delete modfiedProject;
project.SaveToString(modfiedProject);
if (File.WriteAllText(csprojPath, modfiedProject) case .Err)
if (changedContent)
{
Log.EngineLogger.Error($"Failed to modify {csprojPath}");
Log.EngineLogger.Info("Script project file updated, saving...");
String modfiedProject = new String(projectFileContent.Length * 2);
defer delete modfiedProject;
project.SaveToString(modfiedProject);
if (File.WriteAllText(csprojPath, modfiedProject) case .Err)
{
Log.EngineLogger.Error($"Failed to modify {csprojPath}");
}
}
else
{
Log.EngineLogger.Info("Script project file already up to date.");
}
return .Ok;
+10 -2
View File
@@ -375,17 +375,25 @@ class EditorSettings
if (_recentProjects == null)
_recentProjects = new List<String>();
String absolutePath = new String(value.Length);
Path.GetFullPath(value, absolutePath);
if (absolutePath.EndsWith(Path.DirectorySeparatorChar))
absolutePath.RemoveFromEnd(1);
// Remove duplicates
for (var entry in _recentProjects)
{
if (entry == value)
if (entry.EndsWith(Path.DirectorySeparatorChar))
entry.RemoveFromEnd(1);
if (Path.Equals(entry, absolutePath))
{
delete entry;
@entry.Remove();
}
}
_recentProjects.Insert(0, new String(value));
_recentProjects.Insert(0, absolutePath);
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 LoadScriptAssemblyFunc _loadScriptAssembly;
static function void() _unloadAssemblies;
static function void() _unloadScriptAssembly;
static function void() _waitUntilOldAssemblyDead;
// public static unsafe void GetScriptClasses(void** outBuffer, long* 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", "UnloadAssemblies", out _unloadAssemblies);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "UnloadScriptAssembly", out _unloadScriptAssembly);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "WaitUntilOldAssemblyDead", out _waitUntilOldAssemblyDead);
GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "GetScriptClasses", out _getScriptClasses);
@@ -130,14 +134,19 @@ static class CoreClrHelper
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);
}
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.
@@ -16,6 +16,7 @@ class EntitySerializerWrapper : NewScriptClass
function void DeserializeMethod(UUID entityId, void* serializedObjPtr, void* serializerPtr);
function void SerializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr);
function void DeserializeStaticMethod(char8* fullTypeName, void* serializedObjPtr, void* serializerPtr);
function void FinishSerializationMethod(void* serializerPtr);
private CreateSerializationContextMethod _createSerializationContextMethod;
//private ClearSerializationContextMethod _clearSerializationContextMethod;
@@ -25,16 +26,19 @@ class EntitySerializerWrapper : NewScriptClass
private DeserializeMethod _deserializeMethod;
private SerializeStaticMethod _serializeStaticMethod;
private DeserializeStaticMethod _deserializeStaticMethod;
private FinishSerializationMethod _finishSerializationMethod;
[AllowAppend]
public this() : base("GlitchyEngine.Serialization.EntitySerializer", .Empty, .None, false)
{
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "CreateSerializationContext", out _createSerializationContextMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "DestroySerializationContext", out _destroySerializationContextMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Serialize", out _serializeMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_Deserialize", out _deserializeMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_SerializeStaticFields", out _serializeStaticMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_DeserializeStaticFields", out _deserializeStaticMethod);
CoreClrHelper.GetFunctionPointerUnmanagedCallersOnly("GlitchyEngine.ScriptGlue, ScriptCore", "EntitySerializer_FinishSerialization", out _finishSerializationMethod);
}
public void CreateSerializationContext(ScriptInstanceSerializer serializer)
@@ -93,4 +97,10 @@ class EntitySerializerWrapper : NewScriptClass
_deserializeStaticMethod(@class.FullName.CStr(), serializedObjectPtr, serializerPtr);
}
public void FinishSerialization(ScriptInstanceSerializer serializer)
{
void* serializerPtr = Internal.UnsafeCastToPtr(serializer);
_finishSerializationMethod(serializerPtr);
}
}
+50 -51
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
{
// 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.
public static void SetAppAssemblyPath(StringView appAssemblyPath)
public static void SetAppAssemblyPath(StringView appAssemblyPath, bool retainScriptData)
{
_appAssemblyPath.Set(appAssemblyPath);
ReloadAssemblies();
ReloadAssemblies(retainScriptData);
}
static void InitRuntime()
@@ -207,32 +209,6 @@ static class ScriptEngine
CoreClrHelper.Init(ScriptCorePath);
}
/*static void InitMono()
{
Mono.mono_set_assemblies_path("mono/lib/4.5");
if (_debuggingEnabled)
{
char8*[2] options = .(
"--debugger-agent=transport=dt_socket,address=127.0.0.1:2550,server=y,suspend=n,loglevel=3,logfile=MonoDebugger.log",
"--soft-breakpoints"
);
Mono.mono_jit_parse_options(options.Count, &options);
Mono.mono_debug_init(.Mono);
}
s_RootDomain = Mono.mono_jit_init("GlitchyEngineJITRuntime");
Log.EngineLogger.Assert(s_RootDomain != null, "Failed to initialize mono root domain");
if (_debuggingEnabled)
{
Mono.mono_debug_domain_create(s_RootDomain);
}
Mono.mono_thread_set_main(Mono.mono_thread_current());
}*/
static bool _requestingReload = false;
static void InitAssemblyWatcher()
@@ -263,7 +239,7 @@ static class ScriptEngine
Application.Instance.InvokeOnMainThread(new () =>
{
ReloadAssemblies();
ReloadAssemblies(retainScriptData: true);
_requestingReload = false;
_userAssemblyWatcher.StartRaisingEvents();
@@ -299,23 +275,27 @@ static class ScriptEngine
File.ReadAll(pdbPath, pdbData);
CoreClrHelper.LoadAppAssembly(data, pdbData);
CoreClrHelper.LoadScriptAssembly(data, pdbData);
GetEntitiesFromAssemblies();
//ScriptGlue.RegisterManagedComponents();
}
else
{
Log.EngineLogger.Error("Failed to load script assembly, because it doesn't exist.");
}
InitAssemblyWatcher();
}
/// 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!");
Context = context;
ReloadAssemblies();
ReloadAssemblies(retainScriptData);
}
/// Stopts the script runtime and disposes of all script instances.
@@ -435,18 +415,23 @@ static class ScriptEngine
CoreClrHelper.FreeScriptClassNames();
}
public static void ReloadAssemblies()
public static void ReloadAssemblies(bool retainScriptData = true)
{
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.");
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();
@@ -468,20 +453,36 @@ static class ScriptEngine
}
}
contextSerializer.DeserializeScriptInstances();
if (retainScriptData)
{
contextSerializer.DeserializeScriptInstances();
}
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()
{
/*Mono.mono_domain_set(s_RootDomain, false);
Mono.mono_domain_unload(s_AppDomain);
s_AppDomain = null;
Mono.mono_jit_cleanup(s_RootDomain);
s_RootDomain = null;*/
UnloadScriptAssemblies();
}
/// Returns the script class with the given name, or null, if no such class exists.
@@ -492,19 +493,17 @@ static class ScriptEngine
return scriptClass;
}
internal static void LogScriptException(ScriptException exception, UUID entityId)
internal static void LogScriptException(ScriptException exception)
{
String entityInfo = scope .();
if (entityId != .Zero && Context != null)
if (exception.EntityId != .Zero && Context != null)
{
exception.EntityId = entityId;
Result<Entity> sourceEntity = Context.GetEntityByID(entityId);
Result<Entity> sourceEntity = Context.GetEntityByID(exception.EntityId);
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 GlitchyEngine.Core;
using System.Collections;
namespace GlitchyEngine.Scripting;
public class ScriptException
{
private String _fullName ~ delete:append _;
private String _message ~ delete:append _;
private String _stackTrace ~ delete:append _;
/// 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();
private StackTrace _stackTrace ~ delete _;
public StringView FullName => _fullName;
public StringView Message => _message;
public StringView StackTrace => _stackTrace;
public StringView CleanStackTrace => _cleanStackTrace;
public StackTrace StackTrace => _stackTrace;
//public ScriptException InnerException => _innerException;
public UUID EntityId { get; set; }
public UUID EntityId { get; private set; }
[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 allocMessage = append String(message);
String allocStackTrace = append String(stackTrace);
EntityId = entityId;
_stackTrace = ownStackTrace;
_fullName = allocFullExceptionClassName;
_message = allocMessage;
_stackTrace = allocStackTrace;
}
}
// TODO
_cleanStackTrace = _stackTrace;
public class StackFrameInfo
{
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?
class MessageOrigin
{
private UUID _entityId;
private String _fileName ~ delete:append _;
private String _callerMemberName ~ delete:append _;
private int _lineNumber;
private int _columnNumber;
public UUID EntityId => _entityId;
public StringView FileName => _fileName;
public StringView CallerMemberName => _callerMemberName;
public int LineNumber => _lineNumber;
public int ColumnNumber => _columnNumber;
[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 caller = append String(callerMemberName);
_entityId = entityId;
_fileName = file;
_callerMemberName = caller;
_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)]
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]
struct EngineFunctions
{
static Self _functions;
/// Sends the references to all ScriptGlue functions to the script library.
[RegisterMethod]
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)]
public struct RegisterMethodAttribute : Attribute, IOnMethodInit
{
@@ -252,6 +265,7 @@ static class ScriptGlue
private static String _lastExceptionMessage = new .() ~ delete _;
/// Initializes the glue layer between native engine and script library.
public static void Init()
{
if (_setEngineFunctions == null)
@@ -264,6 +278,13 @@ static class ScriptGlue
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()
{
Debug.Profiler.ProfileFunction!();
@@ -278,11 +299,7 @@ static class ScriptGlue
RegisterComponent<MeshRendererComponent>();
}
private static void RegisterCalls()
{
EngineFunctions.FillEngineFunctions();
}
/// Register the function pointers to create, remove and check for components of the specified type and sends them to the script library.
private static void RegisterComponent<T>() where T : struct, new
{
String fullComponentTypeName = scope String();
@@ -445,7 +462,7 @@ static class ScriptGlue
#region Log
[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);
@@ -455,9 +472,7 @@ static class ScriptGlue
if (!fileNamePtr.IsEmpty)
{
String fileName = new:ScopedAlloc! String(fileNamePtr);
MessageOrigin messageOrigin = scope MessageOrigin(fileName, lineNumber);
MessageOrigin messageOrigin = scope MessageOrigin(entityId, fileNamePtr, callerMemberNamePtr, lineNumber, columnNumber);
Log.ClientLogger.Log((LogLevel)logLevel, escapedMessage, messageOrigin);
}
@@ -467,11 +482,27 @@ static class ScriptGlue
}
}
[RegisterCall, CallingConvention(.Cdecl)]
static void Log_LogException(UUID entityId, StringView fullExceptionClassName, StringView exceptionMessage, StringView stackTrace)
[Ordered]
public struct GlueStackFrameInfo
{
ScriptException exception = new ScriptException(entityId, fullExceptionClassName, exceptionMessage, stackTrace);
ScriptEngine.LogScriptException(exception, entityId);
public StringView FileName;
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
@@ -642,7 +673,7 @@ static class ScriptGlue
#endregion
#region TransformComponen
#region TransformComponent
[RegisterCall, CallingConvention(.Cdecl)]
static EngineResult Transform_GetParent(UUID entityId, out UUID parentId)
@@ -966,7 +997,7 @@ static class ScriptGlue
#endregion Rigidbody2D
#region Camer
#region Camera
[RegisterCall, CallingConvention(.Cdecl)]
static void Camera_GetProjectionType(UUID entityId, out SceneCamera.ProjectionType projectionType)
@@ -1534,6 +1565,7 @@ static class ScriptGlue
asset
}*/
/// Set an exception message that the script library can collect when a glue function returned an error.
static void SetExceptionMessage(StringView message)
{
_lastExceptionMessage.Set(message);
@@ -1,6 +1,7 @@
using System.Collections;
using GlitchyEngine.Core;
using GlitchyEngine.Serialization;
using System;
namespace GlitchyEngine.Scripting;
@@ -31,11 +32,22 @@ public class ScriptInstanceSerializer
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.
public void SerializeScriptInstances()
{
Debug.Profiler.ProfileFunction!();
Log.EngineLogger.Trace($"Serializing {Internal.UnsafeCastToPtr(this)}...");
Init();
for (let (id, scriptInstance) in ScriptEngine._entityScriptInstances)
@@ -47,6 +59,8 @@ public class ScriptInstanceSerializer
{
SerializeStaticScriptClassFields(scriptClass);
}
FinishSerialization();
}
/// Serializes the given script instance.
@@ -68,6 +82,8 @@ public class ScriptInstanceSerializer
{
Debug.Profiler.ProfileFunction!();
Log.EngineLogger.Trace($"Deserializing {Internal.UnsafeCastToPtr(this)}...");
Init();
for (let (id, script) in ScriptEngine._entityScriptInstances)
@@ -79,6 +95,8 @@ public class ScriptInstanceSerializer
{
DeserializeStaticScriptClassFields(scriptClass);
}
FinishSerialization();
}
/// 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);
case .EngineObjectReference:
writer.Type("EngineObject");
if (field.Data.EngineObject.FullTypeName.IsEmpty)
if (!((StringView)field.Data.ListEngineObject.FullTypeName).IsEmpty)
writer.Type((StringView)field.Data.ListEngineObject.FullTypeName);
Serialize.Value(writer, field.Data.EngineObject.ID, environment);
case .ObjectReference:
+2 -1
View File
@@ -293,7 +293,8 @@ namespace GlitchyEngine.World
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
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.Generic;
using System.ComponentModel;
@@ -10,12 +17,7 @@ using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Text;
using GlitchyEngine.Core;
using GlitchyEngine.Extensions;
using GlitchyEngine.Graphics;
using GlitchyEngine.Math;
using GlitchyEngine.Math.Attributes;
using ImGuiNET;
using System.Text.Unicode;
using Component = GlitchyEngine.Core.Component;
namespace GlitchyEngine.Editor;
@@ -282,10 +284,10 @@ internal class EntityEditor
if (value is float || value is double || value is Half)
format = "0.#####";
else
format = "0";
format = null;
}
if (!format.StartsWith("%"))
if (format?.StartsWith("%") == false)
{
if (value is float f)
{
@@ -303,14 +305,14 @@ internal class EntityEditor
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;
}
}
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;
}
@@ -327,9 +329,6 @@ internal class EntityEditor
{
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];
switch((char)reference!)
{
@@ -374,20 +373,143 @@ internal class EntityEditor
// Place string delimiter after encoded UTF8 sequence.
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',
'b' => newValue = '\b',
'f' => newValue = '\f',
'n' => newValue = '\n',
'r' => newValue = '\r',
't' => newValue = '\t',
'v' => newValue = '\v',
_ => newValue = '\\'
// It's just the backslash, so we don't need to do anything yet.
if (span.Length == 1)
return 0;
if (span[1] == 'a' || span[1] == 'b' || span[1] == 'f' || span[1] == 'n' ||
span[1] == 'r' || span[1] == 't' || span[1] == 'v' || span[1] == '\0')
{
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
@@ -398,6 +520,10 @@ internal class EntityEditor
newValue = value[0];
}
}
// Show the char as hex code
ImGui.SameLine();
ImGui.TextUnformatted($"(U+{(ushort)(char)reference:X4})");
}
}
else if (fieldType == typeof(byte))
+10 -18
View File
@@ -1,4 +1,5 @@
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
namespace GlitchyEngine;
@@ -11,66 +12,57 @@ public static class Input
/// Returns <see langword="true"/> if the specified key is in the pressed down state.
/// </summary>
/// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsKeyPressed(Key key);
public static bool IsKeyPressed(Key key) => ScriptGlue.Input_IsKeyPressed(key);
/// <summary>
/// Returns <see langword="true"/> if the specified <see cref="key"/> is in the released state.
/// </summary>
/// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsKeyReleased(Key key);
public static bool IsKeyReleased(Key key) => ScriptGlue.Input_IsKeyReleased(key);
/// <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).
/// </summary>
/// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsKeyToggled(Key key);
public static bool IsKeyToggled(Key key) => ScriptGlue.Input_IsKeyToggled(key);
/// <summary>
/// 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).
/// </summary>
/// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsKeyPressing(Key key);
public static bool IsKeyPressing(Key key) => ScriptGlue.Input_IsKeyPressing(key);
/// <summary>
/// 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).
/// </summary>
/// <param name="key">The key to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsKeyReleasing(Key key);
public static bool IsKeyReleasing(Key key) => ScriptGlue.Input_IsKeyReleasing(key);
/// <summary>
/// Returns <see langword="true"/> if the specified mouse button is in the pressed down state.
/// </summary>
/// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsMouseButtonPressed(MouseButton mouseButton);
public static bool IsMouseButtonPressed(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonPressed(mouseButton);
/// <summary>
/// Returns <see langword="true"/> if the specified mouse button is in the released state.
/// </summary>
/// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsMouseButtonReleased(MouseButton mouseButton);
public static bool IsMouseButtonReleased(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonReleased(mouseButton);
/// <summary>
/// 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).
/// </summary>
/// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsMouseButtonPressing(MouseButton mouseButton);
public static bool IsMouseButtonPressing(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonPressing(mouseButton);
/// <summary>
/// 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).
/// </summary>
/// <param name="mouseButton">The button to query.</param>
[MethodImpl(MethodImplOptions.InternalCall)]
public static extern bool IsMouseButtonReleasing(MouseButton mouseButton);
public static bool IsMouseButtonReleasing(MouseButton mouseButton) => ScriptGlue.Input_IsMouseButtonReleasing(mouseButton);
}
+86 -22
View File
@@ -1,7 +1,11 @@
using GlitchyEngine.Core;
using System;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using GlitchyEngine.Core;
using System.Runtime.InteropServices;
using static GlitchyEngine.Log;
namespace GlitchyEngine;
@@ -31,9 +35,10 @@ public class Log
/// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path 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>
@@ -42,9 +47,10 @@ public class Log
/// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path 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>
@@ -53,9 +59,10 @@ public class Log
/// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path 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>
@@ -64,9 +71,10 @@ public class Log
/// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path 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>
@@ -75,9 +83,10 @@ public class Log
/// <param name="message">The message to log.</param>
/// <param name="callerFilePath">File path 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>
@@ -86,9 +95,10 @@ public class Log
/// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path 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>
@@ -97,9 +107,10 @@ public class Log
/// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path 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>
@@ -108,9 +119,10 @@ public class Log
/// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path 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>
@@ -119,9 +131,10 @@ public class Log
/// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path 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>
@@ -130,9 +143,30 @@ public class Log
/// <param name="obj">The object to serialize.</param>
/// <param name="callerFilePath">File path 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>
@@ -141,6 +175,36 @@ public class Log
/// <param name="exception">The exception to log.</param>
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">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<TargetFramework>net10.0</TargetFramework>
<LangVersion>latest</LangVersion>
<BaseOutputPath></BaseOutputPath>
<RootNamespace>GlitchyEngine</RootNamespace>
+133 -7
View File
@@ -8,12 +8,15 @@ using ImGuiNET;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.Metrics;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Loader;
using System.Text;
using System.Threading;
using System.Xml.Linq;
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)
{
_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);
Debug.Assert(_appAssembly != null);
}
private static WeakReference? _weakAssemblyRef;
private static string? _lastAssemblyName;
/// <summary>
/// Called by the engine to unload the assembly (containing user scripts).
/// Called by the engine to unload the assembly containing user scripts.
/// </summary>
[UnmanagedCallersOnly]
public static void UnloadAssemblies()
public static void UnloadScriptAssembly()
{
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;
_appAssembly = null;
GC.Collect();
GC.WaitForPendingFinalizers();
}
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;
[StructLayout(LayoutKind.Sequential, Pack = 0)]
@@ -272,11 +339,16 @@ internal static unsafe partial class ScriptGlue
_unsafeClasses = null;
}
private static readonly AsyncLocal<UUID> _currentEntityId = new ();
internal static UUID CurrentEntityId => _currentEntityId.Value;
[UnmanagedCallersOnly]
public static void ShowEntityEditor(UUID entityId)
{
try
{
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId];
EntityEditor.ShowEntityEditor(entity);
}
@@ -284,6 +356,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[UnmanagedCallersOnly]
@@ -291,6 +367,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnCreate();
}
@@ -298,6 +375,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@@ -305,6 +386,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnUpdate(deltaTime);
}
@@ -312,6 +394,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])]
@@ -319,6 +405,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
(Entity entity, Type type) = EntityScriptInstances[entityId];
entity.OnCollisionEnter2D(collision);
}
@@ -326,6 +413,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[UnmanagedCallersOnly]
@@ -333,6 +424,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
if (EntityScriptInstances.Remove(entityId, out var match) && callDestroy > 0)
{
match.Entity.OnDestroy();
@@ -342,6 +434,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
internal static Type? GetTypeFromNativeString(byte* fullTypeName)
@@ -365,6 +461,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
Type? scriptType = GetTypeFromNativeString(scriptClassName);
Debug.Assert(scriptType != null, "Script class Type not found.");
@@ -379,6 +476,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
struct ComponentFunctionPointers
@@ -459,6 +560,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
if (!EntityScriptInstances.TryGetValue(entityId, out var match))
return;
@@ -468,6 +570,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[UnmanagedCallersOnly]
@@ -475,6 +581,7 @@ internal static unsafe partial class ScriptGlue
{
try
{
_currentEntityId.Value = entityId;
if (!EntityScriptInstances.TryGetValue(entityId, out var match))
return;
@@ -484,6 +591,10 @@ internal static unsafe partial class ScriptGlue
{
Log.Exception(e);
}
finally
{
_currentEntityId.Value = UUID.Zero;
}
}
[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
@@ -544,13 +668,15 @@ internal static unsafe partial class ScriptGlue
public static void Entity_GetScriptInstance(UUID entityId, out Entity? instance)
{
// We currently can implement this method here, because we only have C# scripts.
// If we ever need to do something to interop with other script languages, then this would change.
// If 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))
{
instance = match.Entity;
}
instance = null;
else
{
instance = null;
}
}
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)
{
SerializationObjects.Remove(engineSerializer);
DeserializationObjects.Remove(engineSerializer);
if (SerializationObjects.Remove(engineSerializer, out var serializedObjects))
{
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)
+2 -1
View File
@@ -107,6 +107,7 @@ public class SerializedObject
public void Serialize(Entity entity)
{
_serializedClasses.Add(entity, this);
SerializeInstanceFields(entity);
}
@@ -321,7 +322,7 @@ public class SerializedObject
{
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
{
+25 -2
View File
@@ -122,7 +122,8 @@ public class ScriptGlueGenerator : IIncrementalGenerator
None,
Ref,
In,
Out
Out,
Pointer
}
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);
}
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))
{
return (mappedType, modifier);
@@ -296,6 +306,14 @@ public class ScriptGlueGenerator : IIncrementalGenerator
}
parameterText.Append(parameterType.CSharpTypeName);
switch (parameterModifier)
{
case TypeModifier.Pointer:
parameterText.Append("*");
break;
}
parameterText.Append(", ");
}
@@ -345,7 +363,12 @@ public class ScriptGlueGenerator : IIncrementalGenerator
break;
}
parameterText.Append($"{parameterType.CSharpWrapperType} {param.Name}");
parameterText.Append($"{parameterType.CSharpWrapperType}");
if (parameterModifier == TypeModifier.Pointer)
parameterText.Append("*");
parameterText.Append($" {param.Name}");
switch (parameterModifier)
{