mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 11:40:42 +00:00
Add a troubleshooting panel (#2951)
## Description: Add a troobleshooting panel with the most common problems, and a button to copy the infos for better sharing <img width="893" height="583" alt="image" src="https://github.com/user-attachments/assets/7a37f88c-45b2-448c-86fc-6a3736bc9b25" /> <img width="654" height="697" alt="image" src="https://github.com/user-attachments/assets/11dc1898-579b-42c0-953f-f8237eca2922" /> ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: Mr. Box
This commit is contained in:
@@ -199,6 +199,11 @@
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></user-setting>
|
||||
<troubleshooting-modal
|
||||
id="page-troubleshooting"
|
||||
inline
|
||||
class="hidden w-full h-full page-content"
|
||||
></troubleshooting-modal>
|
||||
<stats-modal
|
||||
id="page-stats"
|
||||
inline
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
},
|
||||
"common": {
|
||||
"close": "Close",
|
||||
"copy": "Copy",
|
||||
"back": "Back",
|
||||
"available": "Available",
|
||||
"preset_max": "Max",
|
||||
@@ -51,12 +52,43 @@
|
||||
"account": "Account",
|
||||
"help": "Help",
|
||||
"menu": "Menu",
|
||||
"troubleshooting": "Troubleshooting",
|
||||
"go_to_troubleshooting": "Go to our troubleshooting page",
|
||||
"pick_pattern": "Pick a pattern!"
|
||||
},
|
||||
"news": {
|
||||
"github_link": "on GitHub",
|
||||
"title": "Release Notes"
|
||||
},
|
||||
"troubleshooting": {
|
||||
"title": "Troubleshooting",
|
||||
"loading": "Loading...",
|
||||
"environment": "Environment",
|
||||
"rendering": "Rendering",
|
||||
"power": "Power",
|
||||
"browser": "Browser",
|
||||
"platform": "Platform",
|
||||
"copied_to_clipboard": "Info copied to the clipboard! Feel free to share it on our Discord if you need help.",
|
||||
"os": "OS",
|
||||
"device_pixel_ratio": "Device Pixel Ratio",
|
||||
"chromium_tip": "OpenFront runs best on Chromium-based browsers.",
|
||||
"hardware_acceleration_tip": "Make sure hardware acceleration is enabled in your browser settings for optimal performance.",
|
||||
"renderer": "Renderer",
|
||||
"max_texture_size": "Max Texture Size",
|
||||
"high_precision_shaders": "High Precision Shaders",
|
||||
"gpu": "GPU",
|
||||
"unavailable": "Unavailable",
|
||||
"gpu_tip": "Verify that this is the dedicated GPU, if one is available.",
|
||||
"battery": "Battery",
|
||||
"charging": "Charging",
|
||||
"battery_level": "Battery Level",
|
||||
"power_saving_tip": "Make sure that your browser is not set to power saving mode.",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"unknown": "Unknown",
|
||||
"software_rendering": "Software rendering",
|
||||
"canvas_2d_no_gpu": "Canvas 2D (no GPU)"
|
||||
},
|
||||
"help_modal": {
|
||||
"hotkeys": "Hotkeys",
|
||||
"table_key": "Key",
|
||||
@@ -141,6 +173,7 @@
|
||||
"build_mirv": "MIRV",
|
||||
"build_mirv_desc": "The most powerful bomb in the game. Splits up into smaller bombs that will cover a huge range of territory. Only damages the player that you first clicked on to build it. Spawns from the nearest Missile Silo and lands in the area you first clicked to build it.",
|
||||
"player_icons": "Player icons",
|
||||
"troubleshooting_desc": "If you experience performance issues, crashes, or other problems while playing OpenFront, please visit our Troubleshooting page for help diagnosing and fixing common issues:",
|
||||
"icon_desc": "Examples of some of the ingame icons you will encounter and what they mean:",
|
||||
"icon_crown": "Crown - Number 1. This is the top player in the leaderboard.",
|
||||
"icon_traitor": "Broken shield - Traitor. This player attacked an ally.",
|
||||
|
||||
+63
-1
@@ -5,6 +5,7 @@ import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/Difficulties";
|
||||
import "./components/Maps";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
import { TroubleshootingModal } from "./TroubleshootingModal";
|
||||
|
||||
@customElement("help-modal")
|
||||
export class HelpModal extends BaseModal {
|
||||
@@ -104,7 +105,7 @@ export class HelpModal extends BaseModal {
|
||||
: ""}"
|
||||
>
|
||||
${modalHeader({
|
||||
title: translateText("main.instructions"),
|
||||
title: translateText("main.help"),
|
||||
onBack: this.close,
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
@@ -120,6 +121,53 @@ export class HelpModal extends BaseModal {
|
||||
[&_p]:text-gray-300 [&_p]:mb-3 [&_strong]:text-white [&_strong]:font-bold
|
||||
scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent"
|
||||
>
|
||||
<!-- Troubleshooting Section -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="text-blue-400">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M2 20 L12 0 L22 20 L2 20"></path>
|
||||
<line x1="12" y1="8" x2="12" y2="14"></line>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"></line>
|
||||
</svg>
|
||||
</div>
|
||||
<h3
|
||||
class="text-xl font-bold uppercase tracking-widest text-white/90"
|
||||
>
|
||||
${translateText("main.troubleshooting")}
|
||||
</h3>
|
||||
<div
|
||||
class="flex-1 h-px bg-gradient-to-r from-blue-500/50 to-transparent"
|
||||
></div>
|
||||
</div>
|
||||
<section>
|
||||
<div class="w-full flex flex-col items-center">
|
||||
<p class="mb-6 text-white/70 text-sm">
|
||||
${translateText("help_modal.troubleshooting_desc")}
|
||||
</p>
|
||||
<button
|
||||
id="troubleshooting-button"
|
||||
class="hover:bg-white/5 px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
data-page="page-troubleshooting"
|
||||
@click="${this.openTroubleshooting}"
|
||||
data-i18n="main.go_to_troubleshooting"
|
||||
>
|
||||
<span
|
||||
class="relative z-10 text-2xl"
|
||||
data-i18n="main.go_to_troubleshooting"
|
||||
></span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<!-- Hotkeys Section -->
|
||||
<div class="flex items-center gap-3 mb-3">
|
||||
<div class="text-blue-400">
|
||||
@@ -1137,6 +1185,20 @@ export class HelpModal extends BaseModal {
|
||||
`;
|
||||
}
|
||||
|
||||
openTroubleshooting() {
|
||||
const troubleshootingModal = document.querySelector(
|
||||
"troubleshooting-modal",
|
||||
) as TroubleshootingModal;
|
||||
if (
|
||||
!troubleshootingModal ||
|
||||
!(troubleshootingModal instanceof TroubleshootingModal)
|
||||
) {
|
||||
console.warn("Troubleshooting modal element not found");
|
||||
return;
|
||||
}
|
||||
troubleshootingModal.open();
|
||||
}
|
||||
|
||||
protected onOpen(): void {
|
||||
this.keybinds = this.getKeybinds();
|
||||
}
|
||||
|
||||
@@ -813,6 +813,7 @@ class Client {
|
||||
"game-top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
"troubleshooting-modal",
|
||||
"territory-patterns-modal",
|
||||
"language-modal",
|
||||
"news-modal",
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
import { html } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { translateText } from "./Utils";
|
||||
import { BaseModal } from "./components/BaseModal";
|
||||
import "./components/baseComponents/Modal";
|
||||
import { modalHeader } from "./components/ui/ModalHeader";
|
||||
import {
|
||||
collectGraphicsDiagnostics,
|
||||
GraphicsDiagnostics,
|
||||
} from "./utilities/Diagnostic";
|
||||
import infoIcon from "/images/InfoIcon.svg?url";
|
||||
|
||||
@customElement("troubleshooting-modal")
|
||||
export class TroubleshootingModal extends BaseModal {
|
||||
@property({ type: String }) markdown = "Loading...";
|
||||
|
||||
@property({ type: Object })
|
||||
diagnostics?: GraphicsDiagnostics;
|
||||
|
||||
@property({ type: Boolean }) loading = true;
|
||||
|
||||
private initialized: boolean = false;
|
||||
|
||||
private async loadDiagnostics() {
|
||||
const canvas = document.createElement("canvas");
|
||||
this.diagnostics = await collectGraphicsDiagnostics(canvas);
|
||||
this.loading = false;
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
render() {
|
||||
const content = html`
|
||||
<div
|
||||
class="h-full select-text flex flex-col ${this.inline
|
||||
? "bg-black/60 backdrop-blur-md rounded-2xl border border-white/10"
|
||||
: ""}"
|
||||
>
|
||||
${modalHeader({
|
||||
titleContent: html` <div
|
||||
class="w-full flex flex-col sm:flex-row justify-between gap-2"
|
||||
>
|
||||
<span
|
||||
class="text-white text-xl sm:text-2xl md:text-3xl font-bold uppercase tracking-widest break-words hyphens-auto"
|
||||
>
|
||||
<a
|
||||
class="hover:text-blue-200 text-blue-400 cursor-pointer"
|
||||
@click=${this.close}
|
||||
>${translateText("main.help")}</a
|
||||
>
|
||||
/ ${translateText("troubleshooting.title")}
|
||||
</span>
|
||||
<button
|
||||
class="hover:bg-white/5 px-6 py-2 text-xs font-bold transition-all duration-200 rounded-lg uppercase tracking-widest bg-blue-500/20 text-blue-400 border border-blue-500/30 shadow-[0_0_15px_rgba(59,130,246,0.2)]"
|
||||
@click=${this.copyDiagnostics}
|
||||
>
|
||||
${translateText("common.copy")}
|
||||
</button>
|
||||
</div>`,
|
||||
onBack: this.close,
|
||||
ariaLabel: translateText("common.back"),
|
||||
})}
|
||||
${this.loading
|
||||
? ""
|
||||
: html`
|
||||
<div
|
||||
class="flex-1 overflow-y-auto px-1 scrollbar-thin scrollbar-thumb-white/20 scrollbar-track-transparent mr-1"
|
||||
>
|
||||
${this.section(
|
||||
"",
|
||||
html`${this.infoTip(
|
||||
translateText("troubleshooting.hardware_acceleration_tip"),
|
||||
true,
|
||||
)}`,
|
||||
)}
|
||||
${this.section(
|
||||
translateText("troubleshooting.environment"),
|
||||
html`
|
||||
${this.row(
|
||||
translateText("troubleshooting.browser"),
|
||||
this.diagnostics!.browser.engine,
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.platform"),
|
||||
this.diagnostics!.browser.platform,
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.os"),
|
||||
this.diagnostics!.browser.os,
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.device_pixel_ratio"),
|
||||
this.diagnostics!.browser.dpr,
|
||||
)}
|
||||
${this.infoTip(
|
||||
translateText("troubleshooting.chromium_tip"),
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
${this.section(
|
||||
translateText("troubleshooting.rendering"),
|
||||
html`
|
||||
${this.row(
|
||||
translateText("troubleshooting.renderer"),
|
||||
this.describeRenderer(this.diagnostics!.rendering),
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.max_texture_size"),
|
||||
this.diagnostics!.rendering.maxTextureSize ??
|
||||
translateText("troubleshooting.unknown"),
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.high_precision_shaders"),
|
||||
this.diagnostics!.rendering.shaderHighp === true
|
||||
? translateText("troubleshooting.yes")
|
||||
: translateText("troubleshooting.no"),
|
||||
)}${this.row(
|
||||
translateText("troubleshooting.gpu"),
|
||||
!this.diagnostics!.rendering.gpu ||
|
||||
this.diagnostics!.rendering.gpu.unavailable
|
||||
? translateText("troubleshooting.unavailable")
|
||||
: `${this.diagnostics!.rendering.gpu.vendor} — ${this.diagnostics!.rendering.gpu.renderer}`,
|
||||
)}
|
||||
${this.infoTip(translateText("troubleshooting.gpu_tip"))}
|
||||
`,
|
||||
)}
|
||||
${this.section(
|
||||
translateText("troubleshooting.power"),
|
||||
html`
|
||||
${this.diagnostics!.power.unavailable
|
||||
? this.row(
|
||||
translateText("troubleshooting.battery"),
|
||||
translateText("troubleshooting.unavailable"),
|
||||
)
|
||||
: html`
|
||||
${this.row(
|
||||
translateText("troubleshooting.charging"),
|
||||
this.diagnostics!.power.charging
|
||||
? translateText("troubleshooting.yes")
|
||||
: translateText("troubleshooting.no"),
|
||||
)}
|
||||
${this.row(
|
||||
translateText("troubleshooting.battery_level"),
|
||||
this.diagnostics!.power.level,
|
||||
)}
|
||||
`}
|
||||
${this.infoTip(
|
||||
translateText("troubleshooting.power_saving_tip"),
|
||||
)}
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
if (this.inline) {
|
||||
return content;
|
||||
}
|
||||
|
||||
return html`
|
||||
<o-modal
|
||||
title=${translateText("troubleshooting.title")}
|
||||
?inline=${this.inline}
|
||||
hideCloseButton
|
||||
hideHeader
|
||||
>
|
||||
${content}
|
||||
</o-modal>
|
||||
`;
|
||||
}
|
||||
|
||||
private infoTip(text: string, warning?: boolean): unknown {
|
||||
return html`
|
||||
<div
|
||||
class="mt-2 ${warning
|
||||
? "bg-orange-500/10"
|
||||
: "bg-white/10"} flex gap-2 text-white py-1 px-3 rounded-sm border-1 ${warning
|
||||
? "border-orange-400"
|
||||
: "border-white/40"}"
|
||||
>
|
||||
<img src="${infoIcon}" class="w-4" />
|
||||
${text}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
protected onOpen(): void {
|
||||
if (!this.initialized) {
|
||||
this.initialized = true;
|
||||
this.loadDiagnostics();
|
||||
}
|
||||
}
|
||||
|
||||
private section(title: string, content: unknown) {
|
||||
return html`
|
||||
<div class="px-4 py-3">
|
||||
<h4
|
||||
class="mb-2 text-xs font-semibold uppercase tracking-wide text-slate-400"
|
||||
>
|
||||
${title}
|
||||
</h4>
|
||||
<div class="space-y-1">${content}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private row(label: string, value: unknown) {
|
||||
return html`
|
||||
<div class="flex justify-between gap-4 text-sm">
|
||||
<span class="text-slate-400">${label}</span>
|
||||
<span class="text-right text-white max-w-100">${value}</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
private async copyDiagnostics() {
|
||||
if (!this.diagnostics) return;
|
||||
const formatted =
|
||||
"```json\n" + JSON.stringify(this.diagnostics, null, 2) + "\n```";
|
||||
await navigator.clipboard.writeText(formatted);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("show-message", {
|
||||
detail: {
|
||||
message: html`${translateText("troubleshooting.copied_to_clipboard")}`,
|
||||
type: "info",
|
||||
duration: 3000,
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private describeRenderer(rendering: any): string {
|
||||
if (rendering.gpu?.software) {
|
||||
return translateText("troubleshooting.software_rendering");
|
||||
}
|
||||
if (rendering.type === "Canvas2D") {
|
||||
return translateText("troubleshooting.canvas_2d_no_gpu");
|
||||
}
|
||||
return `${rendering.type}`;
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.unregisterEscapeHandler();
|
||||
|
||||
if (this.inline) {
|
||||
this.style.pointerEvents = "none";
|
||||
if (window.showPage) {
|
||||
window.showPage?.("page-help");
|
||||
}
|
||||
} else {
|
||||
this.modalEl?.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
export type RendererType = "Canvas2D" | "WebGL1" | "WebGL2";
|
||||
|
||||
export interface BrowserInfo {
|
||||
engine: string;
|
||||
platform: string;
|
||||
os: string;
|
||||
dpr: number;
|
||||
}
|
||||
|
||||
export interface GraphicsDiagnostics {
|
||||
browser: BrowserInfo;
|
||||
rendering: RenderingInfo;
|
||||
power: PowerInfo;
|
||||
}
|
||||
|
||||
export interface GPUInfo {
|
||||
vendor?: string;
|
||||
renderer?: string;
|
||||
software?: boolean;
|
||||
unavailable?: boolean;
|
||||
}
|
||||
|
||||
export interface RenderingInfo {
|
||||
type: RendererType;
|
||||
antialias?: boolean;
|
||||
maxTextureSize?: number;
|
||||
shaderHighp?: boolean;
|
||||
gpu?: GPUInfo;
|
||||
}
|
||||
|
||||
export interface PerformanceInfo {
|
||||
fps: number;
|
||||
worstFrameMs: number;
|
||||
jankPercent: number;
|
||||
throttlingLikely: boolean;
|
||||
}
|
||||
|
||||
export interface PowerInfo {
|
||||
charging?: boolean;
|
||||
level?: string;
|
||||
unavailable?: boolean;
|
||||
}
|
||||
|
||||
export async function collectGraphicsDiagnostics(
|
||||
canvas: HTMLCanvasElement,
|
||||
): Promise<GraphicsDiagnostics> {
|
||||
/* ---------- Browser / OS ---------- */
|
||||
|
||||
const uaData = (navigator as any).userAgentData;
|
||||
|
||||
const os = uaData?.platform ?? detectOS(navigator.userAgent);
|
||||
|
||||
const browser: BrowserInfo = {
|
||||
engine: uaData?.brands
|
||||
? uaData.brands.map((b: any) => b.brand).join(", ")
|
||||
: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
os,
|
||||
dpr: window.devicePixelRatio,
|
||||
};
|
||||
|
||||
/* ---------- Rendering ---------- */
|
||||
|
||||
let gl: WebGLRenderingContext | WebGL2RenderingContext | null = null;
|
||||
let type: RendererType = "Canvas2D";
|
||||
|
||||
gl =
|
||||
canvas.getContext("webgl2", { antialias: true }) ??
|
||||
canvas.getContext("webgl", { antialias: true });
|
||||
|
||||
if (gl) {
|
||||
const isWebGL2 =
|
||||
typeof WebGL2RenderingContext !== "undefined" &&
|
||||
gl instanceof WebGL2RenderingContext;
|
||||
type = isWebGL2 ? "WebGL2" : "WebGL1";
|
||||
}
|
||||
|
||||
const rendering: RenderingInfo = { type };
|
||||
|
||||
if (gl) {
|
||||
rendering.antialias = gl.getContextAttributes()?.antialias ?? false;
|
||||
rendering.maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
||||
|
||||
const precision = gl.getShaderPrecisionFormat(
|
||||
gl.FRAGMENT_SHADER,
|
||||
gl.HIGH_FLOAT,
|
||||
);
|
||||
rendering.shaderHighp = precision !== null && precision.precision > 0;
|
||||
|
||||
const debugInfo = gl.getExtension("WEBGL_debug_renderer_info");
|
||||
|
||||
if (debugInfo) {
|
||||
const renderer = gl.getParameter(
|
||||
(debugInfo as any).UNMASKED_RENDERER_WEBGL,
|
||||
) as string;
|
||||
|
||||
const vendor = gl.getParameter(
|
||||
(debugInfo as any).UNMASKED_VENDOR_WEBGL,
|
||||
) as string;
|
||||
rendering.gpu = {
|
||||
vendor,
|
||||
renderer,
|
||||
software: /swiftshader|llvmpipe|software/i.test(renderer),
|
||||
};
|
||||
} else {
|
||||
rendering.gpu = { unavailable: true };
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Power ---------- */
|
||||
|
||||
let power: PowerInfo = {};
|
||||
|
||||
if ("getBattery" in navigator) {
|
||||
try {
|
||||
const battery = await (navigator as any).getBattery();
|
||||
power = {
|
||||
charging: battery.charging,
|
||||
level: Math.round(battery.level * 100) + "%",
|
||||
};
|
||||
} catch {
|
||||
power = { unavailable: true };
|
||||
}
|
||||
} else {
|
||||
power = { unavailable: true };
|
||||
}
|
||||
return {
|
||||
browser,
|
||||
rendering,
|
||||
power,
|
||||
};
|
||||
}
|
||||
|
||||
function detectOS(ua: string): string {
|
||||
if (/windows nt/i.test(ua)) return "Windows";
|
||||
if (/mac os x/i.test(ua)) return "macOS";
|
||||
if (/android/i.test(ua)) return "Android";
|
||||
if (/iphone|ipad|ipod/i.test(ua)) return "iOS";
|
||||
if (/linux/i.test(ua)) return "Linux";
|
||||
return "Unknown";
|
||||
}
|
||||
Reference in New Issue
Block a user