diff --git a/CringePlugins/CringePlugins.csproj b/CringePlugins/CringePlugins.csproj
index 4be62e5..82f9347 100644
--- a/CringePlugins/CringePlugins.csproj
+++ b/CringePlugins/CringePlugins.csproj
@@ -18,6 +18,7 @@
+
diff --git a/CringePlugins/Loader/PluginWrapper.cs b/CringePlugins/Loader/PluginWrapper.cs
index fccca66..e8f13ba 100644
--- a/CringePlugins/Loader/PluginWrapper.cs
+++ b/CringePlugins/Loader/PluginWrapper.cs
@@ -1,4 +1,6 @@
-using NLog;
+using CringePlugins.Ui;
+using ImGuiNET;
+using NLog;
using VRage.Plugins;
namespace CringePlugins.Loader;
@@ -13,6 +15,8 @@ internal sealed class PluginWrapper(PluginMetadata metadata, IPlugin plugin) : I
private readonly IHandleInputPlugin? _handleInputPlugin = plugin as IHandleInputPlugin;
+ private const float ErrorShowTime = 10f;
+
public void Dispose()
{
try
@@ -23,6 +27,7 @@ internal sealed class PluginWrapper(PluginMetadata metadata, IPlugin plugin) : I
{
Log.Error(e, "Exception while Disposing {Metadata}", metadata);
LastException = e;
+ NotificationsComponent.SpawnNotification(ErrorShowTime, RenderError);
}
}
@@ -39,6 +44,7 @@ internal sealed class PluginWrapper(PluginMetadata metadata, IPlugin plugin) : I
{
Log.Error(e, "Exception while Updating {Metadata}", metadata);
LastException = e;
+ NotificationsComponent.SpawnNotification(ErrorShowTime, RenderError);
}
}
@@ -52,6 +58,7 @@ internal sealed class PluginWrapper(PluginMetadata metadata, IPlugin plugin) : I
{
Log.Error(e, "Exception while Initializing {Metadata}", metadata);
LastException = e;
+ NotificationsComponent.SpawnNotification(ErrorShowTime, RenderError);
}
}
@@ -68,8 +75,16 @@ internal sealed class PluginWrapper(PluginMetadata metadata, IPlugin plugin) : I
{
Log.Error(e, "Exception while Updating {Metadata}", metadata);
LastException = e;
+ NotificationsComponent.SpawnNotification(ErrorShowTime, RenderError);
}
}
public override string ToString() => metadata.ToString();
+
+ private void RenderError()
+ {
+ ImGui.TextColored(new System.Numerics.Vector4(1f, 0f, 0f, 0f), "Error: ");
+ ImGui.SameLine();
+ ImGui.TextWrapped($"Fatal error in {metadata.Name}: {LastException?.Message}");
+ }
}
diff --git a/CringePlugins/Loader/PluginsLifetime.cs b/CringePlugins/Loader/PluginsLifetime.cs
index cba0316..75bd9b4 100644
--- a/CringePlugins/Loader/PluginsLifetime.cs
+++ b/CringePlugins/Loader/PluginsLifetime.cs
@@ -63,6 +63,9 @@ public class PluginsLifetime(string gameFolder) : ILoadingStage
progress.Report("Loading plugins");
+ //we can move this, but it should be before plugin init
+ RenderHandler.Current.RegisterComponent(new NotificationsComponent());
+
await LoadPlugins(cachedPackages, sourceMapping, packagesConfig);
RenderHandler.Current.RegisterComponent(new PluginListComponent(packagesConfig, sourceMapping, configPath, gameFolder, _plugins));
diff --git a/CringePlugins/Ui/NotificationsComponent.cs b/CringePlugins/Ui/NotificationsComponent.cs
new file mode 100644
index 0000000..86d08db
--- /dev/null
+++ b/CringePlugins/Ui/NotificationsComponent.cs
@@ -0,0 +1,102 @@
+using CringePlugins.Abstractions;
+using HarmonyLib;
+using ImGuiNET;
+using System.Numerics;
+using VRage;
+using VRageRender;
+
+namespace CringePlugins.Ui;
+internal class NotificationsComponent : IRenderComponent
+{
+ private static int _globalNotificationId;
+ private const float TransitionTime = .5f;
+ private const float YPadding = 5f;
+
+ private static Vector2 _notificationSize = new(300, 75);
+ private static readonly List Notifications = [];
+
+ private static Size WindowSize => ((Form)MyVRage.Platform.Windows.Window).Size;
+
+ private static float _time;
+
+ public void OnFrame()
+ {
+ var y = 0f;
+
+ var lastY = _notificationSize.Y;
+ var viewportPos = ImGui.GetMainViewport().Pos;
+
+
+ //todo: consider adding a limit to the number of messages that can be displayed at once
+ for (var i = Notifications.Count; i-- > 0;)
+ {
+ var lerpMult = 0f;
+ var notification = Notifications[i];
+ var startDelta = _time - notification.StartTime;
+ var endDelta = _time - notification.HideTime;
+
+ var inTransition = true;
+ if (startDelta is > 0f and < TransitionTime)
+ {
+ lerpMult = startDelta / TransitionTime;
+ }
+ else if (endDelta is > 0f and < TransitionTime)
+ {
+ lerpMult = 1f - (endDelta / TransitionTime);
+ }
+ else if (startDelta > 0f && endDelta < 0f)
+ {
+ lerpMult = 1f;
+ inTransition = false;
+ }
+
+ y += (lastY + YPadding) * EaseInOutCubic(lerpMult);
+ if (!inTransition && y < _notificationSize.Y + YPadding)
+ y = _notificationSize.Y + YPadding;
+
+ ImGui.SetNextWindowPos(new Vector2(WindowSize.Width, y * EaseInOutCubic(lerpMult)) - _notificationSize + viewportPos);
+ ImGui.SetNextWindowSize(new Vector2(_notificationSize.X, 0f));
+
+ Vector2 lastWinSize = Vector2.Zero;
+
+ if (ImGui.Begin($"notification-{notification.GlobalId}", ImGuiWindowFlags.NoMove | ImGuiWindowFlags.NoCollapse | ImGuiWindowFlags.NoTitleBar | ImGuiWindowFlags.NoResize | ImGuiWindowFlags.Tooltip))
+ {
+ notification.RenderCallback?.Invoke();
+
+ //todo: add indicator for when message will expire. probaby just want a rectangle at the bottom of the message
+ //var fraction = Math.Min(1f, (_time - notification.StartTime) / (notification.HideTime - notification.StartTime));
+ //ImGui.ProgressBar(fraction, new Vector2(_notificationSize.X, 10));
+
+ lastWinSize = ImGui.GetWindowSize();
+
+ ImGui.End();
+ }
+
+ lastY = lastWinSize.Y;
+ }
+
+
+ Notifications.RemoveAll(x => x.IsGarbage);
+
+ _time += MyCommon.GetLastFrameDelta();
+ }
+ public static void SpawnNotification(float showTime, Action renderCallback)
+ => Notifications.Add(new NotificationInstruction(_globalNotificationId++, renderCallback, _time, _time + showTime));
+ public static void SpawnNotification(float showTime, string text)
+ => Notifications.Add(new NotificationInstruction(_globalNotificationId++, () => ImGui.TextWrapped(text), _time, _time + showTime));
+
+ private static float EaseInOutCubic(float x) => x < 0.5f
+ ? 4f * x * x * x
+ : 1f - MathF.Pow((-2f * x + 2f), 3f) / 2f;
+
+ private readonly struct NotificationInstruction(int id, Action renderCallback, float startTime, float hideTime)
+ {
+ public readonly int GlobalId = id;
+ public readonly Action RenderCallback = renderCallback;
+ public readonly float StartTime = startTime;
+ public readonly float HideTime = hideTime;
+
+ public readonly bool IsGarbage
+ => _time > HideTime + TransitionTime;
+ }
+}