mirror of
https://github.com/tobspr/shapez.io.git
synced 2025-12-09 16:21:51 +00:00
Re-implement error handler
shapez used to have a nice error handler, but later it was removed. To help debug game in both development and release cases, add a simple error handler/screen that displays the error stack trace, lists the installed mods and shows some build information. It also allows copying this information to the system clipboard. Having such an error screen should make mod development easier once the local mod error handling is removed.
This commit is contained in:
parent
c184d08ece
commit
515c07c067
@ -26,7 +26,7 @@ html {
|
||||
body {
|
||||
color: #555;
|
||||
user-select: none;
|
||||
background: inherit !important;
|
||||
background: inherit;
|
||||
overflow-wrap: break-word;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
89
src/css/error_handler.scss
Normal file
89
src/css/error_handler.scss
Normal file
@ -0,0 +1,89 @@
|
||||
:root {
|
||||
// Provide a fallback font-size for UI scale calculations before the app loads
|
||||
// TODO: perhaps use this as the primary source of UI scale?
|
||||
font-size: #{"round(clamp(1px, calc(min(100vw, 100vh) * 0.019), 100px), 0.01px)"};
|
||||
}
|
||||
|
||||
#errorHandler {
|
||||
--background-primary: hsl(0deg 40% 18%);
|
||||
--background-container: hsl(0deg 40% 10%);
|
||||
--foreground: hsl(0deg 100% 95%);
|
||||
|
||||
--background-button: rgba(255 255 255 / 0.05);
|
||||
--background-button-hover: rgba(255 255 255 / 0.03);
|
||||
--background-button-active: hsl(from var(--background-container) h s l / 0.4);
|
||||
--background-button-success: hsl(130deg 40% 25%);
|
||||
|
||||
background: var(--background-primary);
|
||||
color: var(--foreground);
|
||||
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.3;
|
||||
|
||||
display: grid;
|
||||
padding: 3rem;
|
||||
gap: 0.8rem;
|
||||
|
||||
grid-template-rows: auto 1fr 1fr auto;
|
||||
grid-template-columns: auto minmax(20rem, 20%);
|
||||
|
||||
pre {
|
||||
background: var(--background-container);
|
||||
border-radius: 0.3rem;
|
||||
|
||||
font-size: 85%;
|
||||
padding: 0.2rem 0.4rem;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--background-button);
|
||||
border-radius: 0.3rem;
|
||||
color: inherit;
|
||||
transition: 0.15s ease-out background-color;
|
||||
|
||||
padding: 0.6rem 0.6rem 0.8rem;
|
||||
min-width: 10rem;
|
||||
width: 15%;
|
||||
font-size: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--background-button-hover);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: var(--background-button-active);
|
||||
}
|
||||
|
||||
&.success {
|
||||
background: var(--background-button-success);
|
||||
}
|
||||
}
|
||||
|
||||
.header,
|
||||
.actions {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.stackTrace {
|
||||
grid-row: 2 / 4;
|
||||
}
|
||||
|
||||
.loadedMods,
|
||||
.buildInformation {
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr;
|
||||
gap: 0.2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
padding-top: 1rem;
|
||||
gap: 1rem;
|
||||
justify-content: end;
|
||||
}
|
||||
}
|
||||
@ -13,6 +13,7 @@
|
||||
@import "common";
|
||||
@import "game_state";
|
||||
@import "textual_game_state";
|
||||
@import "error_handler";
|
||||
|
||||
@import "states/preload";
|
||||
@import "states/main_menu";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { AnimationFrame } from "./core/animation_frame";
|
||||
import { BackgroundResourcesLoader } from "./core/background_resources_loader";
|
||||
import { ErrorHandler } from "./core/error_handler";
|
||||
import { GameState } from "./core/game_state";
|
||||
import { setGlobalApp } from "./core/globals";
|
||||
import { InputDistributor } from "./core/input_distributor";
|
||||
@ -41,6 +42,8 @@ export class Application {
|
||||
async boot() {
|
||||
console.log("Booting ...");
|
||||
|
||||
this.errorHandler = new ErrorHandler();
|
||||
|
||||
logger.log("Creating application, platform =", getPlatformName());
|
||||
setGlobalApp(this);
|
||||
|
||||
|
||||
160
src/js/core/error_handler.tsx
Normal file
160
src/js/core/error_handler.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import { MODS } from "@/mods/modloader";
|
||||
import { T } from "@/translations";
|
||||
import copy from "clipboard-copy";
|
||||
import { BUILD_OPTIONS } from "./globals";
|
||||
import { removeAllChildren } from "./utils";
|
||||
|
||||
export class ErrorHandler {
|
||||
isActive = true;
|
||||
|
||||
constructor() {
|
||||
window.addEventListener("error", this.onError.bind(this));
|
||||
window.addEventListener("unhandledrejection", this.onUnhandledRejection.bind(this));
|
||||
}
|
||||
|
||||
private onError(ev: ErrorEvent) {
|
||||
if (!this.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't trigger more than once
|
||||
this.isActive = false;
|
||||
|
||||
const screen = new ErrorScreen(ev.error, ev.filename, ev.lineno, ev.colno);
|
||||
screen.show();
|
||||
}
|
||||
|
||||
private onUnhandledRejection(ev: PromiseRejectionEvent) {
|
||||
const error = ev.reason instanceof Error ? ev.reason : new Error(ev.reason);
|
||||
|
||||
// Avoid logging the error twice
|
||||
ev.preventDefault();
|
||||
|
||||
// Turn the unhandled rejection into a regular error event
|
||||
throw new Error("Unhandled Promise rejection", { cause: error });
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorScreen {
|
||||
private error: Error;
|
||||
private file?: string;
|
||||
private line?: number;
|
||||
private column?: number;
|
||||
|
||||
constructor(error: Error, file?: string, line?: number, column?: number) {
|
||||
this.error = error;
|
||||
this.file = file;
|
||||
this.line = line;
|
||||
this.column = column;
|
||||
}
|
||||
|
||||
show() {
|
||||
// Set the global to stop future callback handlers
|
||||
window.APP_ERROR_OCCURED = true;
|
||||
|
||||
removeAllChildren(document.body);
|
||||
document.body.id = "errorHandler";
|
||||
document.body.className = "";
|
||||
|
||||
const layout = this.createLayout();
|
||||
if (Array.isArray(layout)) {
|
||||
document.body.append(...layout);
|
||||
} else {
|
||||
document.body.append(layout);
|
||||
}
|
||||
}
|
||||
|
||||
private createLayout(): HTMLElement | HTMLElement[] {
|
||||
const btnCopy = <button>{T.errorHandler.actions.copy}</button>;
|
||||
const btnRestart = <button>{T.errorHandler.actions.restart}</button>;
|
||||
|
||||
btnCopy.addEventListener("click", this.copyErrorLog.bind(this));
|
||||
btnRestart.addEventListener("click", this.restart.bind(this));
|
||||
|
||||
return (
|
||||
<>
|
||||
<div class="header">
|
||||
<h1>{T.errorHandler.title}</h1>
|
||||
<div class="source">{this.source}</div>
|
||||
</div>
|
||||
<pre class="stackTrace">{this.recursiveStack}</pre>
|
||||
<div class="loadedMods">
|
||||
{T.errorHandler.labels.loadedMods}
|
||||
<pre>{this.loadedMods}</pre>
|
||||
</div>
|
||||
<div class="buildInformation">
|
||||
{T.errorHandler.labels.buildInformation}
|
||||
<pre>{this.buildInformation}</pre>
|
||||
</div>
|
||||
<div class="actions">
|
||||
{btnCopy}
|
||||
{btnRestart}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private copyErrorLog(ev: MouseEvent) {
|
||||
let log = `shapez Error Log - ${new Date().toISOString()}\n\n`;
|
||||
|
||||
log += this.recursiveStack;
|
||||
log += `\n\nLoaded Mods:\n${this.loadedMods}`;
|
||||
log += `\n\nBuild Information:\n${this.buildInformation}`;
|
||||
|
||||
copy(log);
|
||||
|
||||
if (ev.target instanceof HTMLButtonElement) {
|
||||
ev.target.innerText = T.errorHandler.actions.copyDone;
|
||||
ev.target.classList.add("success");
|
||||
}
|
||||
}
|
||||
|
||||
private restart() {
|
||||
// performRestart may not be available yet
|
||||
location.reload();
|
||||
}
|
||||
|
||||
private get source(): string {
|
||||
return `${this.file} (${this.line}:${this.column})`;
|
||||
}
|
||||
|
||||
private get recursiveStack(): string {
|
||||
// Follow the error cause chain
|
||||
let current = this.error;
|
||||
let stack = current.stack;
|
||||
|
||||
while (current.cause instanceof Error) {
|
||||
current = current.cause;
|
||||
stack += `\nCaused by: ${current.stack}`;
|
||||
}
|
||||
|
||||
return stack;
|
||||
}
|
||||
|
||||
private get loadedMods(): string {
|
||||
const mods: string[] = [];
|
||||
const activeMods = MODS.activeMods;
|
||||
|
||||
for (const mod of MODS.allMods) {
|
||||
const isActive = activeMods.includes(mod);
|
||||
const prefix = isActive ? "*" : "";
|
||||
|
||||
const id = mod.mod.id;
|
||||
const version = mod.mod.metadata.version;
|
||||
|
||||
mods.push(`${prefix}${id}@${version} (${mod.source})`);
|
||||
}
|
||||
|
||||
return mods.join("\n");
|
||||
}
|
||||
|
||||
private get buildInformation(): string {
|
||||
const info: string[] = [];
|
||||
|
||||
for (const [key, value] of Object.entries(BUILD_OPTIONS)) {
|
||||
info.push(`${key}: ${JSON.stringify(value)}`);
|
||||
}
|
||||
|
||||
return info.join("\n");
|
||||
}
|
||||
}
|
||||
@ -1424,3 +1424,13 @@ tips:
|
||||
- Press F4 to show your FPS and Tick Rate.
|
||||
- Press F4 twice to show the tile of your mouse and camera.
|
||||
- You can click a pinned shape on the left side to unpin it.
|
||||
|
||||
errorHandler:
|
||||
title: Unhandled Error!
|
||||
labels:
|
||||
buildInformation: "Build Information:"
|
||||
loadedMods: "Loaded Mods:"
|
||||
actions:
|
||||
copy: Copy
|
||||
copyDone: Copied!
|
||||
restart: Restart
|
||||
|
||||
Loading…
Reference in New Issue
Block a user