diff --git a/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs b/Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs index 24d0d81da..9ee8bee98 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,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 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); @@ -38,7 +44,8 @@ private enum UserChoice { FlushSaveData, RetryLevel, SaveAndQuit, - ReturnToMainMenu + ReturnToMainMenu, + OpenDebugMap } 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(); } } @@ -361,6 +368,8 @@ public void Dispose() { playerRenderTarget = null; } + private UserChoice? MenuChoice; + private IEnumerator Routine() { retry:; if (State != DisplayState.BlueScreen) @@ -381,20 +390,23 @@ private IEnumerator Routine() { Process.Start(new ProcessStartInfo() { FileName = openProg, ArgumentList = { Path.GetDirectoryName(LogFile) }, - UseShellExecute = true + UseShellExecute = true }); })); - 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("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(); @@ -410,19 +422,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) @@ -434,6 +446,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.OpenDebugMap => Session != null, UserChoice.SaveAndQuit => SaveData.Instance != null && Session != null, UserChoice.ReturnToMainMenu => true, _ => false @@ -462,6 +475,15 @@ private bool ExecuteUserChoice(UserChoice choice) { Celeste.Scene = new LevelLoader(Session); break; + case UserChoice.OpenDebugMap: + 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!"); @@ -543,11 +565,14 @@ 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 } + // 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 +603,7 @@ public override void Update() { // Play the get-up animation first playerSprite.Play("rollGetUp"); playerShouldTeabag = true; - crouchTimer = 0; + crouchTimer = 0; } } @@ -591,10 +616,22 @@ public override void Update() { } // Update the options menu - if (Fade == 1) - optMenu?.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; + } } private void BeforeRender() { @@ -667,10 +704,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 +721,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 +751,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) @@ -759,13 +801,32 @@ void DrawLineWrap(string text, float scale, Color color, Vector2 posOff = defaul // since that means the crash was in there 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 + } }