Skip to content
105 changes: 80 additions & 25 deletions Celeste.Mod.mm/Mod/UI/CriticalErrorHandler.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using Celeste.Editor;
using Celeste.Mod.Core;
using Celeste.Mod.Helpers;
using Microsoft.Xna.Framework;
Expand All @@ -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);
Expand All @@ -38,7 +44,8 @@ private enum UserChoice {
FlushSaveData,
RetryLevel,
SaveAndQuit,
ReturnToMainMenu
ReturnToMainMenu,
OpenDebugMap
}

public static CriticalErrorHandler CurrentHandler { get; private set; }
Expand Down Expand Up @@ -136,7 +143,7 @@ static void DoImmediateSceneSwitch(Scene newScene) {
blueScreenScene.Add(CurrentHandler);
CurrentHandler.State = DisplayState.BlueScreen;
blueScreenScene.Entities.UpdateLists();

DoImmediateSceneSwitch(blueScreenScene);
return null;
}
Expand Down Expand Up @@ -205,9 +212,9 @@ static string EvalSafe(Func<string> 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";
}
}
Expand Down Expand Up @@ -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}");
Expand All @@ -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) {
Expand Down Expand Up @@ -317,7 +324,7 @@ public DisplayState State {
private set {
state = value;
if (optMenu != null)
ConfigureOptionsMenu();
ConfigureOptionsMenu();
}
}

Expand Down Expand Up @@ -361,6 +368,8 @@ public void Dispose() {
playerRenderTarget = null;
}

private UserChoice? MenuChoice;

private IEnumerator Routine() {
retry:;
if (State != DisplayState.BlueScreen)
Expand All @@ -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();
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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!");
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -578,7 +603,7 @@ public override void Update() {
// Play the get-up animation first
playerSprite.Play("rollGetUp");
playerShouldTeabag = true;
crouchTimer = 0;
crouchTimer = 0;
}
}

Expand All @@ -591,10 +616,16 @@ 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<KeyboardConfigUI>() == null && Engine.Scene.Tracker.GetEntity<ButtonConfigUI>() == null) {
Comment thread
balt-dev marked this conversation as resolved.
Outdated
CoreModule.Settings.DebugMap.ConsumePress();
NoFade = true;
MenuChoice = UserChoice.OpenDebugMap;
}
}

private void BeforeRender() {
Expand Down Expand Up @@ -667,10 +698,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
Expand All @@ -681,7 +715,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();
}
Expand Down Expand Up @@ -709,7 +745,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)
Expand Down Expand Up @@ -759,13 +795,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<TextMenu.Item> 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

}
}

Expand Down
Loading