From 04ae26a80d539b3962cc4bc671fefa55c135fc89 Mon Sep 17 00:00:00 2001 From: balt-dev Date: Mon, 1 Jun 2026 13:02:33 -0500 Subject: [PATCH 1/4] Add debug editor option and scrolling to critical error handler --- Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index 24d0d81da..78387a105 100644 --- a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs +++ b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs @@ -1,3 +1,4 @@ +using Celeste.Editor; using Celeste.Mod.Core; using Celeste.Mod.Helpers; using Microsoft.Xna.Framework; @@ -17,6 +18,10 @@ namespace Celeste.Mod.UI { public sealed class CriticalErrorHandler : Overlay, IDisposable { + private const int MinimumScrollIndex = 5; + private const float ScrollStrength = 1e-8f; // Exponential easing is weird, so this is small + private float scrollAmount = 0f; + private float scrollTarget = 0f; private static readonly FieldInfo f_Engine_scene = typeof(Engine).GetField("scene", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo f_Engine_nextScene = typeof(Engine).GetField("nextScene", BindingFlags.NonPublic | BindingFlags.Instance); @@ -34,11 +39,13 @@ public enum DisplayState { BlueScreen } - private enum UserChoice { + private enum UserChoice + { FlushSaveData, RetryLevel, SaveAndQuit, - ReturnToMainMenu + ReturnToMainMenu, + OpenDebugEditor } public static CriticalErrorHandler CurrentHandler { get; private set; } @@ -136,7 +143,7 @@ static void DoImmediateSceneSwitch(Scene newScene) { blueScreenScene.Add(CurrentHandler); CurrentHandler.State = DisplayState.BlueScreen; blueScreenScene.Entities.UpdateLists(); - + DoImmediateSceneSwitch(blueScreenScene); return null; } @@ -205,9 +212,9 @@ static string EvalSafe(Func func) { static string FormatByteCount(long bytes) { switch (bytes) { case >= 1024L*1024L*1024L*1024L: return $"{bytes / (1024L*1024L*1024L) / 1024f}TB"; // This should never happen, but just to be future proof .-. - case >= 1024L*1024L*1024L: return $"{bytes / (1024L*1024) / 1024f}GB"; - case >= 1024L*1024L: return $"{bytes / (1024L) / 1024f}MB"; - case >= 1024L: return $"{bytes / 1024f}KB"; + case >= 1024L*1024L*1024L: return $"{bytes / (1024L*1024) / 1024f}GB"; + case >= 1024L*1024L: return $"{bytes / (1024L) / 1024f}MB"; + case >= 1024L: return $"{bytes / 1024f}KB"; default: return $"{bytes}B"; } } @@ -240,7 +247,7 @@ static string FormatByteCount(long bytes) { updatesAvailable = false; } } - + foreach (EverestModule mod in Everest._Modules) { writer.Write($" - {mod.Metadata.Name}: "); writer.Write($"{mod.Metadata.VersionString}"); @@ -261,7 +268,7 @@ static string FormatByteCount(long bytes) { writer.WriteLine($"Crash HResult: {error.HResult}"); writer.WriteLine($"Inner exception (if any): {error.InnerException}"); writer.WriteLine(); - + StackTrace trace = new(error, true); StackFrame latestFrame = trace.GetFrame(0); if (latestFrame != null) { @@ -317,7 +324,7 @@ public DisplayState State { private set { state = value; if (optMenu != null) - ConfigureOptionsMenu(); + ConfigureOptionsMenu(); } } @@ -381,13 +388,14 @@ private IEnumerator Routine() { Process.Start(new ProcessStartInfo() { FileName = openProg, ArgumentList = { Path.GetDirectoryName(LogFile) }, - UseShellExecute = true + UseShellExecute = true }); })); UserChoice? choice = null; if (Session != null) { optMenu.Add(new TextMenu.Button("Retry level") { Disabled = !CanExecuteChoice(UserChoice.RetryLevel) }.Pressed(() => choice = UserChoice.RetryLevel)); + optMenu.Add(new TextMenu.Button("Open debug editor") { Disabled = !CanExecuteChoice(UserChoice.OpenDebugEditor) }.Pressed(() => choice = UserChoice.OpenDebugEditor)); optMenu.Add(new TextMenu.Button("Save & Quit") { Disabled = !CanExecuteChoice(UserChoice.SaveAndQuit) }.Pressed(() => choice = UserChoice.SaveAndQuit)); } @@ -434,6 +442,7 @@ private IEnumerator Routine() { private bool CanExecuteChoice(UserChoice choice) => !failedChoices.Contains(choice) && choice switch { UserChoice.FlushSaveData => SaveData.Instance != null && !hasFlushedSaveData, UserChoice.RetryLevel => Session != null, + UserChoice.OpenDebugEditor => Session != null, UserChoice.SaveAndQuit => SaveData.Instance != null && Session != null, UserChoice.ReturnToMainMenu => true, _ => false @@ -462,6 +471,15 @@ private bool ExecuteUserChoice(UserChoice choice) { Celeste.Scene = new LevelLoader(Session); break; + case UserChoice.OpenDebugEditor: + if (Session == null) { + Logger.Warn("crit-error-handler", "Can't open debug editor as no session is present!"); + return false; + } + + Celeste.Scene = new MapEditor(Session.Area); + break; + case UserChoice.SaveAndQuit: if (Session == null || save == null) { Logger.Warn("crit-error-handler", "Can't save-and-quit as either session or save data is not present!"); @@ -548,6 +566,10 @@ public override void Update() { ovlHandler.Overlay = this; // Restore ourselves as the active overlay } + // Handle button scrolling + ScrollButtons(); + scrollAmount += (scrollTarget - scrollAmount) * MathF.Pow(ScrollStrength, Engine.RawDeltaTime); + // Update the player state if (UsePlayerSprite && playerSprite != null && playerHair != null) { if (playerShouldTeabag) { @@ -578,7 +600,7 @@ public override void Update() { // Play the get-up animation first playerSprite.Play("rollGetUp"); playerShouldTeabag = true; - crouchTimer = 0; + crouchTimer = 0; } } @@ -667,10 +689,13 @@ public override void Render() { // Draw the options menu if (optMenu != null) { optMenu.Alpha = Fade; + optMenu.Position.Y += scrollAmount; optMenu.Render(); if (failedChoices.Count > 0) ActiveFont.Draw("Failed to execute user action", optMenu.Position - Vector2.UnitY * (optMenu.Height * optMenu.Justify.Y + 5), new Vector2(0.5f, 1), new Vector2(0.7f), Color.IndianRed * Fade); + + optMenu.Position.Y -= scrollAmount; } // Draw the player render target to the screen @@ -681,7 +706,9 @@ public override void Render() { try { Vector2 drawPos = new Vector2(Celeste.TargetWidth * 0.15f, Celeste.TargetHeight * 0.5f); float size = Celeste.TargetHeight * 0.65f; + drawPos.Y += scrollAmount; Draw.SpriteBatch.Draw((RenderTarget2D) playerRenderTarget, new Rectangle((int) (drawPos.X - size / 2), (int) (drawPos.Y - size), (int) size, (int) size), Color.White * Fade); + drawPos.Y -= scrollAmount; } finally { HudRenderer.EndRender(); } @@ -709,7 +736,7 @@ void DrawLineWrap(string text, float scale, Color color, Vector2 posOff = defaul if (ActiveFont.Measure(text).X * scale > availSpace) { // Do binary search to determine the cutoff point int start = 0, end = text.Length; - while (start < end -1) { + while (start < end - 1) { int middle = start + (end - start) / 2; float textSize = ActiveFont.Measure(text.Substring(0, middle)).X * scale; if (textSize > availSpace) @@ -757,15 +784,35 @@ void DrawLineWrap(string text, float scale, Color color, Vector2 posOff = defaul for (int i = 0; i < btLines.Length; i++) { // Declutter the stack trace from MonoMod detours, additionally skip the check if it's the latest one, // since that means the crash was in there - if (i != 0 && btLines[i].StartsWith("at Hook<")) continue; + if (i != 0 && btLines[i].StartsWith("at Hook<")) + continue; DrawLineWrap(btLines[i], 0.4f, Color.Gray); - if (textPos.Y >= Celeste.TargetHeight * 0.9f && i+1 < btLines.Length) { + if (textPos.Y >= Celeste.TargetHeight * 0.9f && i + 1 < btLines.Length) { DrawLineWrap("...", 0.5f, Color.Gray); break; } } } + #pragma warning disable CS0618 + private void ScrollButtons() { + if (optMenu is null) return; + + int selectedIndex = 0; + List items = optMenu.GetItems(); + for (int i = 0; i < items.Count; i++) { + if (items[i] == optMenu.Current) { + selectedIndex = i; + break; + } + } + + if (selectedIndex < MinimumScrollIndex) { scrollTarget = 0f; return; } + + scrollTarget = -(selectedIndex - MinimumScrollIndex) * optMenu.Current.Height(); + } + #pragma warning restore CS0618 + } } From 2fcc4c257e63ccaf8c70ba9545a5e09ffaf17ebe Mon Sep 17 00:00:00 2001 From: balt-dev Date: Mon, 1 Jun 2026 15:27:45 -0500 Subject: [PATCH 2/4] Tweak scroll speed of crit error handler and reword debug map option --- Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index 78387a105..28081d8c8 100644 --- a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs +++ b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs @@ -18,10 +18,11 @@ namespace Celeste.Mod.UI { public sealed class CriticalErrorHandler : Overlay, IDisposable { - private const int MinimumScrollIndex = 5; - private const float ScrollStrength = 1e-8f; // Exponential easing is weird, so this is small + private const int MinimumScrollIndex = 3; + private static readonly float ScrollStrength = MathF.Pow(0.3f, 60f); // Exponentiate to adjust for deltatime private float scrollAmount = 0f; private float scrollTarget = 0f; + private bool NoFade = false; private static readonly FieldInfo f_Engine_scene = typeof(Engine).GetField("scene", BindingFlags.NonPublic | BindingFlags.Instance); private static readonly FieldInfo f_Engine_nextScene = typeof(Engine).GetField("nextScene", BindingFlags.NonPublic | BindingFlags.Instance); @@ -45,7 +46,7 @@ private enum UserChoice RetryLevel, SaveAndQuit, ReturnToMainMenu, - OpenDebugEditor + OpenDebugMap } public static CriticalErrorHandler CurrentHandler { get; private set; } @@ -368,6 +369,8 @@ public void Dispose() { playerRenderTarget = null; } + private UserChoice? MenuChoice; + private IEnumerator Routine() { retry:; if (State != DisplayState.BlueScreen) @@ -392,17 +395,19 @@ private IEnumerator Routine() { }); })); - UserChoice? choice = null; + MenuChoice = null; if (Session != null) { - optMenu.Add(new TextMenu.Button("Retry level") { Disabled = !CanExecuteChoice(UserChoice.RetryLevel) }.Pressed(() => choice = UserChoice.RetryLevel)); - optMenu.Add(new TextMenu.Button("Open debug editor") { Disabled = !CanExecuteChoice(UserChoice.OpenDebugEditor) }.Pressed(() => choice = UserChoice.OpenDebugEditor)); - optMenu.Add(new TextMenu.Button("Save & Quit") { Disabled = !CanExecuteChoice(UserChoice.SaveAndQuit) }.Pressed(() => choice = UserChoice.SaveAndQuit)); + if (Celeste.PlayMode == Celeste.PlayModes.Debug) + optMenu.Add(new TextMenu.Button("Open debug map") { Disabled = !CanExecuteChoice(UserChoice.OpenDebugMap) }.Pressed(() => MenuChoice = UserChoice.OpenDebugMap)); + + optMenu.Add(new TextMenu.Button("Retry level") { Disabled = !CanExecuteChoice(UserChoice.RetryLevel) }.Pressed(() => MenuChoice = UserChoice.RetryLevel)); + optMenu.Add(new TextMenu.Button("Save & Quit") { Disabled = !CanExecuteChoice(UserChoice.SaveAndQuit) }.Pressed(() => MenuChoice = UserChoice.SaveAndQuit)); } if (SaveData.Instance != null && !hasFlushedSaveData) - optMenu.Add(new TextMenu.Button("Save current progress") { Disabled = !CanExecuteChoice(UserChoice.FlushSaveData) }.Pressed(() => choice = UserChoice.FlushSaveData)); + optMenu.Add(new TextMenu.Button("Save current progress") { Disabled = !CanExecuteChoice(UserChoice.FlushSaveData) }.Pressed(() => MenuChoice = UserChoice.FlushSaveData)); - optMenu.Add(new TextMenu.Button("Return to main menu") { Disabled = !CanExecuteChoice(UserChoice.ReturnToMainMenu) }.Pressed(() => choice = UserChoice.ReturnToMainMenu)); + optMenu.Add(new TextMenu.Button("Return to main menu") { Disabled = !CanExecuteChoice(UserChoice.ReturnToMainMenu) }.Pressed(() => MenuChoice = UserChoice.ReturnToMainMenu)); optMenu.Add(new TextMenu.Button("Exit Game").Pressed(() => { Scene.OnEndOfFrame += static () => Engine.Instance.Exit(); @@ -418,19 +423,19 @@ private IEnumerator Routine() { Scene prevScene = Celeste.Scene; DisplayState prevState = State; - while (choice == null && prevScene == Celeste.Scene && State == prevState) + while (MenuChoice == null && prevScene == Celeste.Scene && State == prevState) yield return null; if (prevScene != Celeste.Scene || prevState != State) goto retry; // Fade out the menu - if (State != DisplayState.BlueScreen) + if (State != DisplayState.BlueScreen && !NoFade) yield return FadeOut(); // Execute the choice - if (!ExecuteUserChoice(choice.Value)) { - failedChoices.Add(choice.Value); + if (!ExecuteUserChoice(MenuChoice.Value)) { + failedChoices.Add(MenuChoice.Value); goto retry; } if (CurrentHandler == this) @@ -442,7 +447,7 @@ private IEnumerator Routine() { private bool CanExecuteChoice(UserChoice choice) => !failedChoices.Contains(choice) && choice switch { UserChoice.FlushSaveData => SaveData.Instance != null && !hasFlushedSaveData, UserChoice.RetryLevel => Session != null, - UserChoice.OpenDebugEditor => Session != null, + UserChoice.OpenDebugMap => Session != null, UserChoice.SaveAndQuit => SaveData.Instance != null && Session != null, UserChoice.ReturnToMainMenu => true, _ => false @@ -471,7 +476,7 @@ private bool ExecuteUserChoice(UserChoice choice) { Celeste.Scene = new LevelLoader(Session); break; - case UserChoice.OpenDebugEditor: + case UserChoice.OpenDebugMap: if (Session == null) { Logger.Warn("crit-error-handler", "Can't open debug editor as no session is present!"); return false; @@ -617,6 +622,13 @@ public override void Update() { optMenu?.Update(); base.Update(); + + // Handle debug map key + if (Session != null && Celeste.PlayMode == Celeste.PlayModes.Debug && CoreModule.Settings.DebugMap.Pressed && Engine.Scene.Tracker.GetEntity() == null && Engine.Scene.Tracker.GetEntity() == null) { + CoreModule.Settings.DebugMap.ConsumePress(); + NoFade = true; + MenuChoice = UserChoice.OpenDebugMap; + } } private void BeforeRender() { From c83f54bbd3dae7eed5ea23310f9ffee0711d4343 Mon Sep 17 00:00:00 2001 From: Balt <59123926+balt-dev@users.noreply.github.com> Date: Mon, 1 Jun 2026 15:32:45 -0500 Subject: [PATCH 3/4] Fix accidental formatting changes --- Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index 28081d8c8..d117b7af6 100644 --- a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs +++ b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs @@ -40,8 +40,7 @@ public enum DisplayState { BlueScreen } - private enum UserChoice - { + private enum UserChoice { FlushSaveData, RetryLevel, SaveAndQuit, @@ -566,8 +565,7 @@ public override void SceneEnd(Scene scene) { public override void Update() { // Check if another overlay is active if (Scene is IOverlayHandler ovlHandler && ovlHandler.Overlay != this) { - if (ovlHandler.Overlay != null) - return; + if (ovlHandler.Overlay != null) return; ovlHandler.Overlay = this; // Restore ourselves as the active overlay } @@ -618,8 +616,7 @@ public override void Update() { } // Update the options menu - if (Fade == 1) - optMenu?.Update(); + if (Fade == 1) optMenu?.Update(); base.Update(); @@ -796,8 +793,7 @@ void DrawLineWrap(string text, float scale, Color color, Vector2 posOff = defaul for (int i = 0; i < btLines.Length; i++) { // Declutter the stack trace from MonoMod detours, additionally skip the check if it's the latest one, // since that means the crash was in there - if (i != 0 && btLines[i].StartsWith("at Hook<")) - continue; + if (i != 0 && btLines[i].StartsWith("at Hook<")) continue; DrawLineWrap(btLines[i], 0.4f, Color.Gray); if (textPos.Y >= Celeste.TargetHeight * 0.9f && i + 1 < btLines.Length) { DrawLineWrap("...", 0.5f, Color.Gray); From 017c23c2c871481b1ceb722b6ea4033d629edf23 Mon Sep 17 00:00:00 2001 From: Balt <59123926+balt-dev@users.noreply.github.com> Date: Mon, 1 Jun 2026 19:30:24 -0500 Subject: [PATCH 4/4] Change formatting of debug key check in crit error handler --- Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index d117b7af6..9ee8bee98 100644 --- a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs +++ b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs @@ -621,7 +621,13 @@ public override void Update() { base.Update(); // Handle debug map key - if (Session != null && Celeste.PlayMode == Celeste.PlayModes.Debug && CoreModule.Settings.DebugMap.Pressed && Engine.Scene.Tracker.GetEntity() == null && Engine.Scene.Tracker.GetEntity() == null) { + if ( + Session != null && + Celeste.PlayMode == Celeste.PlayModes.Debug && + CoreModule.Settings.DebugMap.Pressed && + Engine.Scene.Tracker.GetEntity() == null && + Engine.Scene.Tracker.GetEntity() == null + ) { CoreModule.Settings.DebugMap.ConsumePress(); NoFade = true; MenuChoice = UserChoice.OpenDebugMap;