Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 86 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,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<KeyboardConfigUI>() == null &&
Engine.Scene.Tracker.GetEntity<ButtonConfigUI>() == null
) {
CoreModule.Settings.DebugMap.ConsumePress();
NoFade = true;
MenuChoice = UserChoice.OpenDebugMap;
}
}

private void BeforeRender() {
Expand Down Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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<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