From 04ae26a80d539b3962cc4bc671fefa55c135fc89 Mon Sep 17 00:00:00 2001 From: balt-dev Date: Mon, 1 Jun 2026 13:02:33 -0500 Subject: [PATCH 1/5] 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/5] 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/5] 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/5] 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; From 6bc925ed636d1ada69d6001ad09cda8a171e65a7 Mon Sep 17 00:00:00 2001 From: balt-dev Date: Sat, 20 Jun 2026 23:09:20 -0500 Subject: [PATCH 5/5] Clean up the UI code for the critical error handler --- Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs | 201 +++++++++++------- 1 file changed, 121 insertions(+), 80 deletions(-) diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index 9ee8bee98..6619937b5 100644 --- a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs +++ b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs @@ -18,11 +18,11 @@ namespace Celeste.Mod.UI { public sealed class CriticalErrorHandler : Overlay, IDisposable { - 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 float PlayerSpriteSize = MaxPlayerSpriteSize; + private const float MaxPlayerSpriteSize = 0.5f; + private const float MaxOptionsHeight = Celeste.TargetHeight * 0.9f; + private const float MaxOptionSpacing = 4; 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); @@ -50,7 +50,7 @@ private enum UserChoice { public static CriticalErrorHandler CurrentHandler { get; private set; } - public static ExceptionDispatchInfo HandleCriticalError(ExceptionDispatchInfo error) { + public static ExceptionDispatchInfo HandleCriticalError(ExceptionDispatchInfo error, DisplayState? targetState = null) { if (!CoreModule.Settings.UseInGameCrashHandler) return error; @@ -69,6 +69,8 @@ public static ExceptionDispatchInfo HandleCriticalError(ExceptionDispatchInfo er CurrentHandler.EncounteredAdditionalErrors = true; AmendLogFile(CurrentHandler.LogFile, "Encountered an additional critical error", error.SourceException); } + if (targetState is { } state) + CurrentHandler.State = state; // Invoke the critical error event Everest.Events.CriticalError(CurrentHandler); @@ -121,10 +123,11 @@ static void DoImmediateSceneSwitch(Scene newScene) { CurrentHandler.RemoveSelf(); oldScene?.Entities?.UpdateLists(); - Scene errorScene = new Scene(); - errorScene.Add(new HudRenderer()); - errorScene.Add(new HiresSnow()); - errorScene.Add(CurrentHandler); + Scene errorScene = new Scene { + new HudRenderer(), + new HiresSnow(), + CurrentHandler + }; CurrentHandler.State = DisplayState.CleanScene; errorScene.Entities.UpdateLists(); @@ -371,12 +374,9 @@ public void Dispose() { private UserChoice? MenuChoice; private IEnumerator Routine() { - retry:; - if (State != DisplayState.BlueScreen) - yield return FadeIn(); - else - Fade = 1f; + Fade = 0f; + retry: ; // Create the options menu optMenu = new TextMenu() { AutoScroll = false }; @@ -388,17 +388,16 @@ private IEnumerator Routine() { throw new NotSupportedException(); Process.Start(new ProcessStartInfo() { - FileName = openProg, - ArgumentList = { Path.GetDirectoryName(LogFile) }, - UseShellExecute = true + FileName = openProg, + ArgumentList = { Path.GetDirectoryName(LogFile) }, + UseShellExecute = true }); })); - MenuChoice = null; if (Session != null) { 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)); } @@ -418,6 +417,13 @@ private IEnumerator Routine() { ConfigureOptionsMenu(); + if (State != DisplayState.BlueScreen) + yield return FadeIn(); + else + Fade = 1f; + + MenuChoice = null; + // Wait for the user to make a choice, or the scene to change, or the display state to change Scene prevScene = Celeste.Scene; DisplayState prevState = State; @@ -458,11 +464,8 @@ private bool ExecuteUserChoice(UserChoice choice) { SaveData save = SaveData.Instance; switch (choice) { case UserChoice.FlushSaveData: - if (save != null) { - save.BeforeSave(); - UserIO.Save(SaveData.GetFilename(save.FileSlot), UserIO.Serialize(save)); - patch_UserIO.ForceSerializeModSave(); - } + if (save != null) + ManuallySaveGame(); hasFlushedSaveData = true; return true; // We don't want to reset the overlay yet @@ -492,9 +495,7 @@ private bool ExecuteUserChoice(UserChoice choice) { // Do a save manually Session.InArea = true; - save.BeforeSave(); - UserIO.Save(SaveData.GetFilename(save.FileSlot), UserIO.Serialize(save)); - patch_UserIO.ForceSerializeModSave(); + ManuallySaveGame(); // Load the overworld Celeste.Scene = new OverworldLoader(Overworld.StartMode.MainMenu); @@ -516,23 +517,65 @@ private bool ExecuteUserChoice(UserChoice choice) { return true; } + // Forcibly saves the game. + private void ManuallySaveGame() { + if (SaveData.Instance is null) return; + SaveData.Instance.BeforeSave(); + UserIO.Save(SaveData.GetFilename(SaveData.Instance.FileSlot), UserIO.Serialize(SaveData.Instance)); + patch_UserIO.ForceSerializeModSave(); + } + + // Sets up the options and player sprite visually. + // NOTE: To be honest, I'm not entirely sure why this code works. Good luck to you if you alter it. private void ConfigureOptionsMenu() { - optMenu.ItemSpacing = 4; + // First, we find how much space we have worst-case + optMenu.ItemSpacing = 0; optMenu.RecalculateSize(); - - if (UsePlayerSprite) { - optMenu.Position = new Vector2(Celeste.TargetWidth * 0.15f, Celeste.TargetHeight * 0.55f); - optMenu.Justify = new Vector2(0.5f, 0f); - - // Reduce item spacing if there are too many items - if (optMenu.Position.Y + optMenu.Height > Celeste.TargetHeight * 0.85f) { - optMenu.ItemSpacing = 0; - optMenu.RecalculateSize(); + PlayerSpriteSize = MaxPlayerSpriteSize; + + float originalSpriteSize = Celeste.TargetHeight * PlayerSpriteSize; + + // Check how much space we have left over + float verticalDeadSpace = MaxOptionsHeight - optMenu.Height; + if (UsePlayerSprite) verticalDeadSpace -= originalSpriteSize; + if (verticalDeadSpace < 0 && UsePlayerSprite) { + // We need to shrink the player sprite to fit the screen + PlayerSpriteSize = Math.Min((originalSpriteSize + verticalDeadSpace) / Celeste.TargetHeight, MaxPlayerSpriteSize); + if (PlayerSpriteSize <= 0) { + Logger.Warn("crit-error-handler", "Not enough vertical space for player sprite, disabling"); + disablePlayerSprite = true; + PlayerSpriteSize = 0; } - } else { - optMenu.Position = new Vector2(Celeste.TargetWidth * 0.15f, Celeste.TargetHeight * 0.6f); - optMenu.Justify = new Vector2(0.5f, 0.5f); + + verticalDeadSpace += originalSpriteSize - PlayerSpriteSize * Celeste.TargetHeight; } + + int optionCount = ((patch_TextMenu)optMenu).Items.Count; + + float optionSpacing = verticalDeadSpace / optionCount; + + if (optionSpacing < 0) + // We are completely out of room. RIP. We intentionally don't clamp optionSpacing to >=0 to keep everything on screen even with overlap + Logger.Warn("crit-error-handler", $"Not enough vertical space for options menu, forcing overlap"); + + optMenu.ItemSpacing = Math.Min(optionSpacing, MaxOptionSpacing); + optMenu.RecalculateSize(); + + if (optMenu.Height - 1f > MaxOptionsHeight) // -1f to give a leeway for float shenanigans + Logger.Warn("crit-error-handler", $"Got incorrect height for option menu, is this a bug? (Expected {MaxOptionsHeight}, got {optMenu.Height})"); + + // We center the options within the available space ONLY if the player sprite isn't there, + // since it looks fine uncentered with the player but awful without + float availableSpace = MaxOptionsHeight - optMenu.Height; + + float optY = (Celeste.TargetHeight - MaxOptionsHeight) / 2f; + if (UsePlayerSprite) + optY += Celeste.TargetHeight * PlayerSpriteSize; + else + optY += availableSpace / 2; + + optMenu.Position = new Vector2(Celeste.TargetWidth * 0.15f, optY + optMenu.Height / 2); + optMenu.Justify = new Vector2(0.5f, 0.5f); } public override void Added(Scene scene) { @@ -569,10 +612,6 @@ 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) { @@ -619,18 +658,24 @@ public override void Update() { if (Fade == 1) 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; + + // Handle option keys + if (Engine.Scene.Tracker.GetEntity() == null && Engine.Scene.Tracker.GetEntity() == null) { + if (Session != null) { + if (Celeste.PlayMode == Celeste.PlayModes.Debug && CoreModule.Settings.DebugMap.Pressed) { + CoreModule.Settings.DebugMap.ConsumePress(); + NoFade = true; + MenuChoice = UserChoice.OpenDebugMap; + } else if (Input.MenuCancel.Pressed) { + Input.MenuCancel.ConsumePress(); + NoFade = true; + MenuChoice = UserChoice.RetryLevel; + } + } else if (Input.MenuCancel.Pressed) { + Input.MenuCancel.ConsumePress(); + NoFade = true; + MenuChoice = UserChoice.ReturnToMainMenu; + } } } @@ -704,13 +749,10 @@ 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 @@ -719,11 +761,13 @@ public override void Render() { try { HudRenderer.BeginRender(sampler: SamplerState.PointClamp); 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; + Vector2 drawPos = new(Celeste.TargetWidth * 0.15f, (Celeste.TargetHeight - MaxOptionsHeight) / 2); + float size = Celeste.TargetHeight * PlayerSpriteSize; + Draw.SpriteBatch.Draw( + (RenderTarget2D) playerRenderTarget, + new Rectangle((int) (drawPos.X - size / 2), (int) drawPos.Y, (int) size, (int) size), + Color.White * Fade + ); } finally { HudRenderer.EndRender(); } @@ -808,25 +852,22 @@ void DrawLineWrap(string text, float scale, Color color, Vector2 posOff = defaul } } - #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; - } + [Command("criterror", "Shows the Everest critical error handler. Optionally can take a display state of 'initial', 'overlay', or 'cleanscene'.")] + internal static void CmdCriticalError(string displayState = "initial") { + if (displayState is null) { + Engine.Commands.Log("Expected a display state of .", Color.Yellow); + return; } - if (selectedIndex < MinimumScrollIndex) { scrollTarget = 0f; return; } - - scrollTarget = -(selectedIndex - MinimumScrollIndex) * optMenu.Current.Height(); + DisplayState state = displayState switch { + "overlay" => DisplayState.Overlay, + "cleanscene" => DisplayState.CleanScene, + _ => DisplayState.Initial, + }; + Exception exc = new(Calc.Random.NextSingle() > 0.95 ? "smots gaming" : "Hello, world!"); + ExceptionDispatchInfo.SetCurrentStackTrace(exc); + HandleCriticalError(ExceptionDispatchInfo.Capture(exc), state); } - #pragma warning restore CS0618 - } }