diff --git a/GlitchyEditor/SandboxProject/Assets/Scripts/MyTestEntity.cs b/GlitchyEditor/SandboxProject/Assets/Scripts/MyTestEntity.cs index 051544c..4432357 100644 --- a/GlitchyEditor/SandboxProject/Assets/Scripts/MyTestEntity.cs +++ b/GlitchyEditor/SandboxProject/Assets/Scripts/MyTestEntity.cs @@ -83,6 +83,22 @@ namespace Sandbox Log.Error("Camera not found."); } } + Log.Warning("Achtung."); + + try + { + SubVoid(); + } + catch (Exception e) + { + Console.WriteLine(e); + throw e; + } + } + + void SubVoid() + { + throw new Exception("Ouha!", new IndexOutOfRangeException("Bist du jecke2?!", new AccessViolationException("Haleluja"))); } /// @@ -113,6 +129,9 @@ namespace Sandbox { force.Y += JumpForce; } + + if (Input.IsKeyPressing(Key.N)) + SubVoid(); _rigidBody.ApplyForceToCenter(force); diff --git a/GlitchyEditor/content/Textures/EditorIcons.dds b/GlitchyEditor/content/Textures/EditorIcons.dds index 036696a..1ebc060 100644 Binary files a/GlitchyEditor/content/Textures/EditorIcons.dds and b/GlitchyEditor/content/Textures/EditorIcons.dds differ diff --git a/GlitchyEditor/content/Textures/EditorIcons.dds.ass b/GlitchyEditor/content/Textures/EditorIcons.dds.ass index 1f70168..94df200 100644 --- a/GlitchyEditor/content/Textures/EditorIcons.dds.ass +++ b/GlitchyEditor/content/Textures/EditorIcons.dds.ass @@ -9,8 +9,8 @@ AddressModeU = .Clamp, AddressModeV = .Clamp, AddressModeW = .Clamp, - MipMinLOD = -340282346638528859811704183484516925440, - MipMaxLOD = 340282346638528859811704183484516925440, + MipMinLOD = -3.40282347e+38, + MipMaxLOD = 3.40282347e+38, MaxAnisotropy = 1, BorderColor = { R = 1, diff --git a/GlitchyEditor/content/Textures/EditorIcons.psd b/GlitchyEditor/content/Textures/EditorIcons.psd index 8b53a30..01d9928 100644 Binary files a/GlitchyEditor/content/Textures/EditorIcons.psd and b/GlitchyEditor/content/Textures/EditorIcons.psd differ diff --git a/GlitchyEditor/src/EditWindows/LogWindow.bf b/GlitchyEditor/src/EditWindows/LogWindow.bf new file mode 100644 index 0000000..d5b3a03 --- /dev/null +++ b/GlitchyEditor/src/EditWindows/LogWindow.bf @@ -0,0 +1,367 @@ +using System; +using ImGui; +using System.Collections; +using GlitchyEngine.Core; +using GlitchyEngine.World; +using GlitchLog; +using GlitchyEngine.Scripting; +using GlitchyEngine.Renderer; + +namespace GlitchyEditor.EditWindows; + +enum MessageType +{ + case None = 0; + case Trace = 1; + case Info = 2; + case Warning = 4; + case Error = 8; + + public this(LogLevel level) + { + switch (level) + { + case .Error: + this = Error; + case .Warning: + this = Warning; + case .Info: + this = Info; + case .Trace: + this = Trace; + default: + this = None; + } + } +} + +class MessageSource +{ + public UUID? Entity = null; + public StringView? ScriptName = null; + public int? Line = null; + + /// If true, the message is only meant for engine developers... so only me :( + public bool IsEngineMessage = false; + + public MonoExceptionHelper Exception = null ~ _?.ReleaseRef(); + + public String AdditionalData = null ~ delete _; +} + +class LogMessage +{ + private String _message ~ delete _; + + public MessageType MessageType { get; private set; } + + public DateTime Timestamp { get; private set; } + + public MessageSource Source { get; private set; } ~ delete _; + + public StringView Message => _message; + + public this(DateTime timestamp, StringView message, MessageType logLevel, MessageSource ownSource) + { + Timestamp = timestamp; + _message = new String(message); + MessageType = logLevel; + Source = ownSource; + } +} + +class LogWindow : EditorWindow +{ + public const String s_WindowTitle = "Log"; + + private append List _messages = .() ~ ClearAndDeleteItems!(_); + + public static SubTexture2D s_ErrorIcon; + public static SubTexture2D s_WarningIcon; + public static SubTexture2D s_InfoIcon; + public static SubTexture2D s_TraceIcon; + + private bool _showGameMessages = true; + private bool _showEngineMessages = false; + private bool _autoScroll = true; + private bool _collapseMessages = true; + + private MessageType _visibleMessageTypes = .Error | .Warning | .Info | .Trace; + + protected override void InternalShow() + { + defer { ImGui.End(); } + if(!ImGui.Begin(s_WindowTitle, &_open, .MenuBar)) + return; + + ShowMenuBar(); + + ShowMessages(); + } + + private void ShowMenuBar() + { + if(ImGui.BeginMenuBar()) + { + if (ImGui.MenuItem("Clear")) + { + ClearLog(); + } + + if (ImGui.BeginMenu("Filter")) + { + ImGui.Checkbox("Show game messages", &_showGameMessages); + ImGui.AttachTooltip("If checked, the log will show messages generated by the game (e.g. scripts)."); + + ImGui.Checkbox("Show engine messages", &_showEngineMessages); + ImGui.AttachTooltip(""" + If checked, the log will show messages generated by the engine. + These messages are usually only necessary for engine debugging/development and don't provide practical information for game developers. + """); + + ImGui.EndMenu(); + } + + ImGui.Checkbox("Collapse", &_collapseMessages); + ImGui.AttachTooltip("If checked, identical messages will be collapsed into one."); + + var col = ImGui.GetStyleColorVec4(.Button); + + if (_visibleMessageTypes.HasFlag(.Trace)) + ImGui.PushStyleColor(.Button, *col); + else + ImGui.PushStyleColor(.Button, .(0, 0, 0, 0)); + + if (ImGui.ImageButtonEx(1, s_TraceIcon, .(14, 14), .Zero, .Ones, .(2, 2))) + _visibleMessageTypes ^= .Trace; + + ImGui.PopStyleColor(); + + if (_visibleMessageTypes.HasFlag(.Info)) + ImGui.PushStyleColor(.Button, *col); + else + ImGui.PushStyleColor(.Button, .(0, 0, 0, 0)); + + if (ImGui.ImageButtonEx(2, s_InfoIcon, .(16, 16))) + _visibleMessageTypes ^= .Info; + + ImGui.PopStyleColor(); + + if (_visibleMessageTypes.HasFlag(.Warning)) + ImGui.PushStyleColor(.Button, *col); + else + ImGui.PushStyleColor(.Button, .(0, 0, 0, 0)); + + if (ImGui.ImageButtonEx(3, s_WarningIcon, .(16, 16))) + _visibleMessageTypes ^= .Warning; + + ImGui.PopStyleColor(); + + if (_visibleMessageTypes.HasFlag(.Error)) + ImGui.PushStyleColor(.Button, *col); + else + ImGui.PushStyleColor(.Button, .(0, 0, 0, 0)); + + if (ImGui.ImageButtonEx(4, s_ErrorIcon, .(16, 16))) + _visibleMessageTypes ^= .Error; + + ImGui.PopStyleColor(); + + ImGui.EndMenuBar(); + } + } + + private void ShowMessages() + { + if (ImGui.BeginTable("Messages", 3, .BordersInnerH | .SizingFixedFit)) + { + ImGui.TableSetupColumn("", .WidthFixed); + ImGui.TableSetupColumn("", .WidthStretch); + ImGui.TableSetupColumn("", .WidthFixed); + + //LogMessage lastMessage = null; + int count = 1; + + // Message ID for ImGui + int imGuiMessageId = 0; + + for (let message in _messages) + { + if (!_visibleMessageTypes.HasFlag(message.MessageType)) + continue; + + if ((message.Source.IsEngineMessage && !_showEngineMessages) || (!message.Source.IsEngineMessage && !_showGameMessages)) + continue; + + /*defer + { + lastMessage = message; + }*/ + + do + { + LogMessage lastMessage = (message != _messages.Back) ? _messages[@message.Index + 1] : null; + + if (_collapseMessages && lastMessage != null) + { + if (message.MessageType != lastMessage.MessageType) + break; + + if (message.Message != lastMessage.Message) + break; + + if (message.Source.Entity != lastMessage.Source.Entity) + break; + + // For exceptions the stack trace is basically the only relevant thing + if (message.Source.Exception?.StackTrace != lastMessage.Source.Exception?.StackTrace) + break; + + // We collapse this message with the previous one: + // Increment counter and go to next message. + count++; + continue; + } + } + + // Push current index as ID + ImGui.PushID((void*)++imGuiMessageId); + + ImGui.TableNextRow(); + ImGui.TableSetColumnIndex(0); + + switch (message.MessageType) + { + case .Error: + ImGui.Image(s_ErrorIcon, ImGui.Vec2(32, 32)); + ImGui.AttachTooltip("Error"); + case .Warning: + ImGui.Image(s_WarningIcon, ImGui.Vec2(32, 32)); + ImGui.AttachTooltip("Warning"); + case .Info: + ImGui.Image(s_InfoIcon, ImGui.Vec2(32, 32)); + ImGui.AttachTooltip("Info"); + case .Trace: + ImGui.Image(s_TraceIcon, ImGui.Vec2(32, 32)); + ImGui.AttachTooltip("Trace"); + default: + ImGui.TextUnformatted("Unknown"); + } + + ImGui.TableNextColumn(); + + // Timestamp + ImGui.TextWrapped($"[{message.Timestamp:HH:mm:ss.fff}]"); + + if (message.Source.IsEngineMessage) + { + ImGui.SameLine(); + ImGui.TextUnformatted("Engine"); + } + + // Show entity + if (message.Source?.Entity != null) + { + ImGui.SameLine(); + + Result entity = Editor.Instance.CurrentScene.GetEntityByID(message.Source.Entity.Value); + + if (entity case .Ok(let e)) + { + ImGui.Text($"Entity: \"{e.Name}\" (ID: {message.Source.Entity})"); + + if (ImGui.IsItemClicked()) + Editor.Instance.EntityHierarchyWindow.HighlightEntity(entity); + } + else + { + ImGui.Text($"Entity: (ID: {message.Source.Entity})"); + } + } + + if (message.Source.Exception != null) + { + if (ImGui.CollapsingHeader(message.Message.Ptr)) + { + // Show the native to managed entry point only if we show engine messages + + if (_showEngineMessages) + ImGui.TextUnformatted(message.Source.Exception.StackTrace); + else + ImGui.TextUnformatted(message.Source.Exception.CleanStackTrace); + + ImGui.NewLine(); + } + } + else + { + ImGui.TextUnformatted(message.Message); + } + + // Dont show the counter if we only have one message. + if (count > 1) + { + // the message is not collapsible with the previous one. + // Print message count for last message and reset counter. + // This message will be printed normally. + + ImGui.TableNextColumn(); + ImGui.Text($"{count}"); + } + + count = 1; + + ImGui.PopID(); + } + + if (_autoScroll) + { + if (ImGui.GetIO().MouseWheel > 0) + { + _autoScroll = false; + } + else + { + ImGui.SetScrollY(ImGui.GetScrollMaxY()); + } + } + else + { + if (ImGui.GetScrollMaxY() == ImGui.GetScrollY()) + { + _autoScroll = true; + } + } + + ImGui.EndTable(); + } + } + + public void ClearLog() + { + ClearAndDeleteItems!(_messages); + } + + public void Log(DateTime timestamp, LogLevel severity, StringView message, MessageSource source) + { + LogMessage logMessage = new LogMessage(timestamp, message, MessageType(severity), source); + _messages.Add(logMessage); + } + + public void LogException(DateTime timestamp, MonoExceptionHelper 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"); + + // TODO: are mono exceptions never engine only? + LogMessage logMessage = new LogMessage(timestamp, message, .Error, new MessageSource(){Entity = exception.Instance, Exception = exception..AddRef(), IsEngineMessage = false}); + _messages.Add(logMessage); + } +} \ No newline at end of file diff --git a/GlitchyEditor/src/Editor.bf b/GlitchyEditor/src/Editor.bf index 75224d8..b52aaa0 100644 --- a/GlitchyEditor/src/Editor.bf +++ b/GlitchyEditor/src/Editor.bf @@ -22,6 +22,7 @@ namespace GlitchyEditor private ContentBrowserWindow _contentBrowserWindow ~ delete _; private PropertiesWindow _propertiesWindow ~ delete _; private AssetViewer _assetViewer ~ delete _; + private LogWindow _logWindow ~ delete _; public Scene CurrentScene { @@ -45,6 +46,7 @@ namespace GlitchyEditor public ContentBrowserWindow ContentBrowserWindow => _contentBrowserWindow; public PropertiesWindow PropertiesWindow => _propertiesWindow; public AssetViewer AssetViewer => _assetViewer; + public LogWindow LogWindow => _logWindow; public EditorCamera* CurrentCamera { get; set; } @@ -69,6 +71,11 @@ namespace GlitchyEditor InitWindows(); } + public ~this() + { + s_Instance = null; + } + private void InitWindows() { _sceneViewportWindow = new EditorViewportWindow(this); @@ -78,6 +85,7 @@ namespace GlitchyEditor _contentBrowserWindow = new ContentBrowserWindow((.)Application.Get().ContentManager); _propertiesWindow = new PropertiesWindow(this); _assetViewer = new AssetViewer((.)Application.Get().ContentManager); + _logWindow = new LogWindow(); } public void Update() @@ -89,6 +97,7 @@ namespace GlitchyEditor _contentBrowserWindow.Show(); _propertiesWindow.Show(); _assetViewer.Show(); + _logWindow.Show(); } } } diff --git a/GlitchyEditor/src/EditorApp.bf b/GlitchyEditor/src/EditorApp.bf index a4ccec8..f9436b8 100644 --- a/GlitchyEditor/src/EditorApp.bf +++ b/GlitchyEditor/src/EditorApp.bf @@ -12,6 +12,9 @@ namespace GlitchyEditor public this(String[] args) { PushLayer(new EditorLayer(args, _contentManager)); + + Log.ClientLogger = new EditorLogger(); + Log.EngineLogger = new EditorLogger() { IsEngineLogger = true }; } protected override IContentManager InitContentManager() diff --git a/GlitchyEditor/src/EditorIcons.bf b/GlitchyEditor/src/EditorIcons.bf index c335d5e..796b94d 100644 --- a/GlitchyEditor/src/EditorIcons.bf +++ b/GlitchyEditor/src/EditorIcons.bf @@ -19,6 +19,10 @@ namespace GlitchyEditor public SubTexture2D Simulate ~ _.ReleaseRef(); public SubTexture2D Pause ~ _.ReleaseRef(); public SubTexture2D SingleStep ~ _.ReleaseRef(); + public SubTexture2D Error ~ _.ReleaseRef(); + public SubTexture2D Warning ~ _.ReleaseRef(); + public SubTexture2D Info ~ _.ReleaseRef(); + public SubTexture2D Trace ~ _.ReleaseRef(); public SamplerState SamplerState { @@ -41,6 +45,10 @@ namespace GlitchyEditor Simulate = GetNextGridTexture(ref pen, iconSize); Pause = GetNextGridTexture(ref pen, iconSize); SingleStep = GetNextGridTexture(ref pen, iconSize); + Error = GetNextGridTexture(ref pen, iconSize); + Warning = GetNextGridTexture(ref pen, iconSize); + Info = GetNextGridTexture(ref pen, iconSize); + Trace = GetNextGridTexture(ref pen, iconSize); } private SubTexture2D GetNextGridTexture(ref float2 pen, float2 iconSize) diff --git a/GlitchyEditor/src/EditorLayer.bf b/GlitchyEditor/src/EditorLayer.bf index a35da5e..b48497e 100644 --- a/GlitchyEditor/src/EditorLayer.bf +++ b/GlitchyEditor/src/EditorLayer.bf @@ -171,6 +171,11 @@ namespace GlitchyEditor ContentBrowserWindow.s_FolderTexture = _editorIcons.Folder; ContentBrowserWindow.s_FileTexture = _editorIcons.File; + + LogWindow.s_ErrorIcon = _editorIcons.Error; + LogWindow.s_WarningIcon = _editorIcons.Warning; + LogWindow.s_InfoIcon = _editorIcons.Info; + LogWindow.s_TraceIcon = _editorIcons.Trace; } private void InitEditor() @@ -807,6 +812,9 @@ namespace GlitchyEditor if(ImGui.MenuItem(AssetViewer.s_WindowTitle)) _editor.AssetViewer.Open = true; + + if(ImGui.MenuItem(LogWindow.s_WindowTitle)) + _editor.LogWindow.Open = true; ImGui.EndMenu(); } diff --git a/GlitchyEditor/src/EditorLogger.bf b/GlitchyEditor/src/EditorLogger.bf new file mode 100644 index 0000000..cca6085 --- /dev/null +++ b/GlitchyEditor/src/EditorLogger.bf @@ -0,0 +1,128 @@ +using GlitchLog; +using System; +using System.Diagnostics; +using GlitchyEngine.Scripting; + +namespace GlitchyEditor; + +public class EditorLogger : Logger +{ + // {l} = log level (first parameter) + // {t} = current date time (second parameter) + // {n} = logger name (third parameter) + // {m} = message + + private String _name; + + public override String Name + { + get => _name; + set => _name = value; + } + + public bool IsEngineLogger {get; set;} + + public this() + { + //Debug.Assert(Debug.IsDebuggerPresent, "The DebugLogger requires a debugger to be present."); + } + +#if GL_NOLOG || GL_LOG_NOTRACE + [SkipCall] +#endif + [Inline] + public override void Trace(StringView format, params Object[] args) + { + InternalLog(.Trace, format, params args); + } + +#if GL_NOLOG || GL_LOG_NOINFO + [SkipCall] +#endif + [Inline] + public override void Info(StringView format, params Object[] args) + { + InternalLog(.Info, format, params args); + } + +#if GL_NOLOG || GL_LOG_NOWARNING + [SkipCall] +#endif + [Inline] + public override void Warning(StringView format, params Object[] args) + { + InternalLog(.Warning, format, params args); + } + +#if GL_NOLOG || GL_LOG_NOERROR + [SkipCall] +#endif + [Inline] + public override void Error(StringView format, params Object[] args) + { + InternalLog(.Error, format, params args); + } + +#if GL_NOLOG || GL_LOG_NOCRITICAL + [SkipCall] +#endif + [Inline] + public override void Critical(StringView format, params Object[] args) + { + InternalLog(.Critical, format, params args); + } + +#if GL_NOLOG + [SkipCall] +#endif + [Inline] + public override void Log(LogLevel level, StringView format, params Object[] args) + { + InternalLog(level, format, params args); + } + + public override void Assert(bool condition, String error = Compiler.CallerExpression[0], String filePath = Compiler.CallerFilePath, int line = Compiler.CallerLineNum) + { + if (!condition) + { + String failStr = scope .()..AppendF("Assert failed: {} at line {} in {}", error, line, filePath); + InternalLog(.Critical, failStr); + Internal.FatalError(failStr, 1); + } + } + + public override void AssertDebug(bool condition, String error = Compiler.CallerExpression[0], String filePath = Compiler.CallerFilePath, int line = Compiler.CallerLineNum) + { + if (!condition) + { + String failStr = scope .()..AppendF("Assert failed: {} at line {} in {}", error, line, filePath); + InternalLog(.Critical, failStr); + Internal.FatalError(failStr, 1); + } + } + + private void InternalLog(LogLevel level, StringView format, params Object[] args) + { + if(_logLevel > level) + return; + + DateTime timestamp = DateTime.Now; + + String message = scope String(4096); + message.AppendF(format, params args); + + Debug.Write($"[{timestamp:HH:mm:ss.fff}] ({_name})|{level.UpperString}: {message}"); + + if (Editor.Instance == null) + return; + + if (args.Count > 0 && (var ex = args[^1] as MonoExceptionHelper)) + { + Editor.Instance.LogWindow.LogException(timestamp, ex); + } + else + { + Editor.Instance.LogWindow.Log(timestamp, level, message, new .() {IsEngineMessage = IsEngineLogger}); + } + } +} diff --git a/GlitchyEngine/src/ImGui/ImGuiExtension.bf b/GlitchyEngine/src/ImGui/ImGuiExtension.bf index 890f627..758119b 100644 --- a/GlitchyEngine/src/ImGui/ImGuiExtension.bf +++ b/GlitchyEngine/src/ImGui/ImGuiExtension.bf @@ -96,9 +96,21 @@ namespace ImGui return ImageButton(subTexture.Texture.GetViewBinding(), size, (.)subTexture.TexCoords.XY, (.)v, frame_padding, bg_col, tint_col); } - + public static extern bool ImageButton(TextureViewBinding textureViewBinding, Vec2 size, Vec2 uv0 = Vec2.Zero, Vec2 uv1 = Vec2.Ones, int32 frame_padding = -1, Vec4 bg_col = Vec4.Zero, Vec4 tint_col = Vec4.Ones); - + + public static bool ImageButtonEx(uint32 id, SubTexture2D subTexture, Vec2 size, Vec2 uv0 = Vec2.Zero, Vec2 uv1 = Vec2.Ones, Vec2 frame_padding = .Zero, Vec4 bg_col = Vec4.Zero, Vec4 tint_col = Vec4.Ones) + { + if (uv0 != .Zero || uv1 != .Ones) + Runtime.NotImplemented(); + + float2 v = (.)subTexture.TexCoords.XY + subTexture.TexCoords.ZW; + + return ImageButtonEx(id, subTexture.Texture.GetViewBinding(), size, (.)subTexture.TexCoords.XY, (.)v, frame_padding, bg_col, tint_col); + } + + public static extern bool ImageButtonEx(uint32 id, TextureViewBinding textureViewBinding, Vec2 size, Vec2 uv0 = Vec2.Zero, Vec2 uv1 = Vec2.Ones, Vec2 frame_padding = .Zero, Vec4 bg_col = Vec4.Zero, Vec4 tint_col = Vec4.Ones); + public static void TextUnformatted(StringView text) => TextUnformattedImpl(text.Ptr, text.Ptr + text.Length); public static void PushID(StringView id) => PushID(id.Ptr, id.Ptr + id.Length); diff --git a/GlitchyEngine/src/Platform/DX11/ImGui/ImGui.bf b/GlitchyEngine/src/Platform/DX11/ImGui/Dx11ImGui.bf similarity index 72% rename from GlitchyEngine/src/Platform/DX11/ImGui/ImGui.bf rename to GlitchyEngine/src/Platform/DX11/ImGui/Dx11ImGui.bf index b886d84..9e561b6 100644 --- a/GlitchyEngine/src/Platform/DX11/ImGui/ImGui.bf +++ b/GlitchyEngine/src/Platform/DX11/ImGui/Dx11ImGui.bf @@ -34,6 +34,18 @@ namespace ImGui return pressed; } + public static override bool ImageButtonEx(uint32 id, TextureViewBinding textureViewBinding, Vec2 size, Vec2 uv0 = Vec2.Zero, Vec2 uv1 = Vec2.Ones, Vec2 frame_padding = .Zero, Vec4 bg_col = Vec4.Zero, Vec4 tint_col = Vec4.Ones) + { + var view = textureViewBinding._nativeShaderResourceView..AddRef(); + _resourceViews.Add(view); + + bool pressed = ImGui.ImageButtonEx(id, view, size, uv0, uv1, frame_padding, bg_col, tint_col); + + textureViewBinding.Release(); + + return pressed; + } + protected internal static override void CleanupFrame() { for(var view in _resourceViews) diff --git a/GlitchyEngine/src/Scripting/MonoExceptionHelper.bf b/GlitchyEngine/src/Scripting/MonoExceptionHelper.bf new file mode 100644 index 0000000..1bcb271 --- /dev/null +++ b/GlitchyEngine/src/Scripting/MonoExceptionHelper.bf @@ -0,0 +1,81 @@ +using System; +using GlitchyEngine.Core; +using Mono; + +namespace GlitchyEngine.Scripting; + + +public class MonoExceptionHelper : RefCounter +{ + private String _fullName ~ delete _; + + private String _message ~ delete _; + + private String _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; + + private MonoExceptionHelper _innerException ~ _?.ReleaseRef(); + + public StringView FullName => _fullName; + public StringView Message => _message; + + public StringView StackTrace => _stackTrace; + public StringView CleanStackTrace => _cleanStackTrace; + + public MonoExceptionHelper InnerException => _innerException; + + public UUID Instance { get; set; } + + public this(MonoException* exception) + { + MonoObject* exObject = (MonoObject*)exception; + + MonoClass* monoClass = Mono.mono_object_get_class(exObject); + + StringView classNamespace = .(Mono.mono_class_get_namespace(monoClass)); + StringView className = .(Mono.mono_class_get_name(monoClass)); + _fullName = new $"{classNamespace}.{className}"; + + GetMessage(exObject, monoClass); + + GetStackTrace(exception); + + GetInnerException(exObject, monoClass); + } + + private void GetMessage(MonoObject* exceptionObject, MonoClass* monoClass) + { + var messageProperty = Mono.mono_class_get_property_from_name(monoClass, "Message"); + + MonoObject* message = Mono.mono_property_get_value(messageProperty, exceptionObject, null, null); + char8* exMessage = Mono.mono_string_to_utf8((.)message); + + _message = new String(exMessage); + + Mono.mono_free(exMessage); + } + + private void GetStackTrace(MonoException* exception) + { + char8* stacktracePtr = Mono.mono_exception_get_managed_backtrace(exception); + _stackTrace = new String(stacktracePtr); + + int entryIndex = _stackTrace.IndexOf("at (wrapper native-to-managed)"); + + if (entryIndex != -1) + _cleanStackTrace = _stackTrace.Substring(0, entryIndex); + else + _cleanStackTrace = _stackTrace; + } + + private void GetInnerException(MonoObject* exceptionObject, MonoClass* monoClass) + { + MonoProperty* innerExceptionProperty = Mono.mono_class_get_property_from_name(monoClass, "InnerException"); + + MonoObject* innerException = Mono.mono_property_get_value(innerExceptionProperty, exceptionObject, null, null); + + if (innerException != null) + _innerException = new MonoExceptionHelper((MonoException*)innerException); + } +} diff --git a/GlitchyEngine/src/Scripting/ScriptClass.bf b/GlitchyEngine/src/Scripting/ScriptClass.bf index e3998cc..53908de 100644 --- a/GlitchyEngine/src/Scripting/ScriptClass.bf +++ b/GlitchyEngine/src/Scripting/ScriptClass.bf @@ -227,59 +227,40 @@ class ScriptClass : SharpClass _onDestroy = (OnDestroyMethod)GetMethodThunk("OnDestroy"); } - public void OnCreate(MonoObject* instance) + public void OnCreate(MonoObject* instance, out MonoException* exception) { - MonoException* exception = null; + exception = null; + if (_onCreate != null) _onCreate(instance, &exception); } - public void OnUpdate(MonoObject* instance, float deltaTime) + public void OnUpdate(MonoObject* instance, float deltaTime, out MonoException* exception) { - MonoException* exception = null; + exception = null; + if (_onUpdate != null) _onUpdate(instance, deltaTime, &exception); - - if (exception != null) - { - char8* str = Mono.mono_string_to_utf8(exception.Message); - - Log.EngineLogger.Error($"Exception in \"{_fullName}.OnUpdate\". Message:\"{StringView(str)}\""); - - Mono.mono_free(str); - } } - public void OnDestroy(MonoObject* instance) + public void OnDestroy(MonoObject* instance, out MonoException* exception) { - MonoException* exception; + exception = null; + if (_onDestroy != null) _onDestroy(instance, &exception); } - public MonoObject* CreateInstance(UUID uuid) + public MonoObject* CreateInstance(UUID uuid, out MonoException* exception) { MonoObject* instance = Mono.mono_object_new(ScriptEngine.[Friend]s_AppDomain, _monoClass); - // TODO: I think this is a bit dirty // Invoke empty constructor to fill fields Mono.mono_runtime_object_init(instance); // Invoke constructor with UUID #unwarn - ScriptEngine.[Friend]s_EngineObject.Invoke(ScriptEngine.[Friend]s_EngineObject._constructor, instance, &uuid); - - //MonoException* exception = null; -//#unwarn - //ScriptEngine.[Friend]s_EntityRoot._constructor(instance, uuid, &exception); - //ScriptEngine.[Friend]s_EntityRoot.Invoke(_constructor, instance, &uuid); - - /*MonoObject* exception = null; -#unwarn*/ - //Mono.mono_runtime_invoke(_constructor, instance, (.)&uuid, &exception); - //Mono.mono_runtime_object_init(instance); - //MonoException* exception; - //_constructor(instance, uuid, &exception); + ScriptEngine.[Friend]s_EngineObject.Invoke(ScriptEngine.[Friend]s_EngineObject._constructor, instance, out exception, &uuid); return instance; } @@ -312,12 +293,22 @@ class ScriptClass : SharpClass public MonoObject* Invoke(MonoMethod* method, MonoObject* instance, void** args = null) { MonoObject* exception = null; - return Mono.mono_runtime_invoke(method, instance, args, &exception); + + MonoObject* result = Mono.mono_runtime_invoke(method, instance, args, &exception); + + if (exception != null) + ScriptEngine.HandleMonoException((MonoException*)exception); + + return result; } - public MonoObject* Invoke(MonoMethod* method, MonoObject* instance, params void*[] args) + public MonoObject* Invoke(MonoMethod* method, MonoObject* instance, out MonoException* exception, params void*[] args) { - return Mono.mono_runtime_invoke(method, instance, args.Ptr, null); + exception = null; + + MonoObject* result = Mono.mono_runtime_invoke(method, instance, args.Ptr, (.)&exception); + + return result; } public T Invoke(MonoMethod* method, MonoObject* instance, params void*[] args) @@ -329,25 +320,17 @@ class ScriptClass : SharpClass public T GetFieldValue(MonoObject* instance, MonoClassField* field) { T value = default; - Mono.Mono.mono_field_get_value(instance, field, &value); + Mono.mono_field_get_value(instance, field, &value); return value; } public void SetFieldValue(MonoObject* instance, MonoClassField* field, in T value) { - /*if (typeof(T) == typeof(MonoObject*)) - { - // TODO: MonoObject* is a pointer already, so we don't take the pointer - Mono.Mono.mono_field_set_value(instance, field, (void*)value); - } - else - {*/ - Mono.Mono.mono_field_set_value(instance, field, &value); - //} + Mono.mono_field_set_value(instance, field, &value); } public void SetFieldValue(MonoObject* instance, MonoClassField* field, in T value) where T : struct* { - Mono.Mono.mono_field_set_value(instance, field, value); + Mono.mono_field_set_value(instance, field, value); } } diff --git a/GlitchyEngine/src/Scripting/ScriptEngine.bf b/GlitchyEngine/src/Scripting/ScriptEngine.bf index eb8c122..21a58ec 100644 --- a/GlitchyEngine/src/Scripting/ScriptEngine.bf +++ b/GlitchyEngine/src/Scripting/ScriptEngine.bf @@ -209,12 +209,14 @@ static class ScriptEngine if (scriptClass == null) return false; - script.Instance = new ScriptInstance(scriptClass); + UUID entityId = entity.UUID; + + script.Instance = new ScriptInstance(entityId, scriptClass); script.Instance..ReleaseRef(); - _entityScriptInstances[entity.UUID] = script.Instance..AddRef(); + _entityScriptInstances[entityId] = script.Instance..AddRef(); - script.Instance.Instantiate(entity.UUID); + script.Instance.Instantiate(entityId); return true; } @@ -544,4 +546,27 @@ static class ScriptEngine return scriptClass; } -} \ No newline at end of file + + internal static void HandleMonoException(MonoException* exception, ScriptInstance sourceInstance = null) + { + MonoExceptionHelper wrappedException = new MonoExceptionHelper(exception); + + String entityInfo = scope .(); + + if (sourceInstance != null) + { + wrappedException.Instance = sourceInstance.EntityId; + + Result sourceEntity = Context.GetEntityByID(sourceInstance.EntityId); + + if (sourceEntity case .Ok(let e)) + { + entityInfo.AppendF($" ({e.Name} | {sourceInstance.EntityId})"); + } + } + + Log.ClientLogger.Error($"Mono Exception \"{wrappedException.FullName}\": \"{wrappedException.Message}\"{entityInfo}\nStackTrace:\n{wrappedException.StackTrace}", wrappedException); + + wrappedException.ReleaseRef(); + } +} diff --git a/GlitchyEngine/src/Scripting/ScriptInstance.bf b/GlitchyEngine/src/Scripting/ScriptInstance.bf index 1511444..70aa425 100644 --- a/GlitchyEngine/src/Scripting/ScriptInstance.bf +++ b/GlitchyEngine/src/Scripting/ScriptInstance.bf @@ -4,6 +4,8 @@ using System; namespace GlitchyEngine.Scripting; +using internal GlitchyEngine.Scripting; + class ScriptInstance : RefCounter { private ScriptClass _scriptClass; @@ -11,6 +13,8 @@ class ScriptInstance : RefCounter private MonoObject* _instance; private uint32 _gcHandle; + private UUID _entityId; + public ScriptClass ScriptClass => _scriptClass; /// Gets whether or not the instance has ben initialized. @@ -23,9 +27,13 @@ class ScriptInstance : RefCounter internal MonoObject* MonoInstance => _instance; - public this(ScriptClass scriptClass) + public UUID EntityId => _entityId; + + public this(UUID entityId, ScriptClass scriptClass) { Log.EngineLogger.AssertDebug(scriptClass != null); + + _entityId = entityId; _scriptClass = scriptClass..AddRef(); } @@ -33,7 +41,7 @@ class ScriptInstance : RefCounter { if (_instance != null) { - _scriptClass.OnDestroy(_instance); + InvokeOnDestroy(); Mono.mono_gchandle_free(_gcHandle); } _scriptClass?.ReleaseRef(); @@ -41,24 +49,36 @@ class ScriptInstance : RefCounter public void Instantiate(UUID uuid) { - _instance = _scriptClass.CreateInstance(uuid); + _instance = _scriptClass.CreateInstance(uuid, let exception); _gcHandle = Mono.mono_gchandle_new(_instance, true); + + if (exception != null) + ScriptEngine.HandleMonoException(exception, this); } public void InvokeOnCreate() { - _scriptClass.OnCreate(_instance); + _scriptClass.OnCreate(_instance, let exception); _isCreated = true; + + if (exception != null) + ScriptEngine.HandleMonoException(exception, this); } public void InvokeOnUpdate(float deltaTime) { - _scriptClass.OnUpdate(_instance, deltaTime); + _scriptClass.OnUpdate(_instance, deltaTime, let exception); + + if (exception != null) + ScriptEngine.HandleMonoException(exception, this); } public void InvokeOnDestroy() { - _scriptClass.OnDestroy(_instance); + _scriptClass.OnDestroy(_instance, let exception); + + if (exception != null) + ScriptEngine.HandleMonoException(exception, this); } public T GetFieldValue(ScriptField field) @@ -83,6 +103,9 @@ class ScriptInstance : RefCounter Mono.mono_property_set_value(entityProperty, componentInstance, (void**)&_instance, &exception); + if (exception != null) + ScriptEngine.HandleMonoException((MonoException*)exception, this); + return componentInstance; } } \ No newline at end of file diff --git a/GlitchyEngineHelper/src/Mono/Mono.bf b/GlitchyEngineHelper/src/Mono/Mono.bf index 4e7d896..72780f7 100644 --- a/GlitchyEngineHelper/src/Mono/Mono.bf +++ b/GlitchyEngineHelper/src/Mono/Mono.bf @@ -232,12 +232,25 @@ static class Mono [LinkName(.C)] public static extern MonoThread* mono_thread_current(); - + +#region Property + [LinkName(.C)] public static extern MonoProperty* mono_class_get_property_from_name(MonoClass *klass, char8* name); [LinkName(.C)] public static extern void mono_property_set_value(MonoProperty *prop, void *obj, void **@params, MonoObject **exc); + + [LinkName(.C)] + public static extern MonoObject* mono_property_get_value(MonoProperty *prop, void *obj, void** @params, MonoObject** exc); + +#endregion + + [LinkName(.C)] + public static extern char8* mono_exception_get_managed_backtrace(MonoException* exc); + + [LinkName(.C)] + public static extern MonoClass* mono_object_get_class(MonoObject* obj); } struct MonoDomain;