diff --git a/package.json b/package.json index 13aa64e..1ef136c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "wok-client", "productName": "WOK Client", - "version": "1.1.0-rc.17", + "version": "1.1.0-rc.18", "private": true, "description": "The fastest Krunker client. Ever.", "main": "./bundle/main.mjs", diff --git a/src/intro-window.ts b/src/intro-window.ts index dc49c8d..7699e78 100644 --- a/src/intro-window.ts +++ b/src/intro-window.ts @@ -38,6 +38,8 @@ export interface IntroGameWindowHandoff { handleReadyToShow(): void; revealBehindIntro(): void; revealForUse(): void; + /** True while the intro owns the game window's first reveal (between beginIntro and end). */ + isRevealOwned(): boolean; } export function createIntroGameWindowHandoff(gameWindow: IntroGameWindow, fullscreenMode: string): IntroGameWindowHandoff { let introActive = false; @@ -60,6 +62,7 @@ export function createIntroGameWindowHandoff(gameWindow: IntroGameWindow, fullsc endIntro: () => { introActive = false; }, + isRevealOwned: () => introActive, handleReadyToShow: () => { if (!introActive) reveal(true); }, diff --git a/src/main.ts b/src/main.ts index 9895810..5379cbc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2436,6 +2436,18 @@ app.on('ready', async () => { logPerfMark('loadurl-called'); const gameNavigation = mainWindow.loadURL('https://krunker.io'); + /* + * Show the hidden game window as soon as the navigation is under way, with or without the + * intro. A hidden renderer receives no compositor frames, so Krunker's load can stall before + * ready-to-show on affected machines and the launch deadlocks; showing the window (which the + * intro path always does at its opaque reveal) keeps the load moving, and the black splash + * background in the preload covers the unstyled page until readiness. + */ + if (!introHandoff.isRevealOwned() && !mainWindow.isDestroyed() && !mainWindow.isVisible()) { + mainWindow.showInactive(); + logPerfMark('window-revealed-without-intro'); + } + // Start the intro only once the game navigation is under way, so the identity animation can // never delay the first byte of Krunker's load. Calibration launches skip it: that flow has // already put windows in front of the user and should reach the game as directly as possible. diff --git a/src/menu-declutter.ts b/src/menu-declutter.ts index 2985f81..0d6a127 100644 --- a/src/menu-declutter.ts +++ b/src/menu-declutter.ts @@ -358,8 +358,8 @@ export function collectMenuDeclutterTargets( } } - const streamPromotions = environment - .queryAll('.stream-card.promo-card') + const streamPromotions = [...environment + .queryAll('.stream-card.promo-card')] .filter(isKrunkerStreamPromotionCard); for (const promotion of streamPromotions) targets.add(promotion); @@ -368,7 +368,7 @@ export function collectMenuDeclutterTargets( for (const promotion of streamPromotions) { const section = promotion.closest('.featured-section'); if (!section) continue; - const cards = section.querySelectorAll('.stream-card'); + const cards = [...section.querySelectorAll('.stream-card')]; if (cards.length > 0 && cards.every(isKrunkerStreamPromotionCard)) targets.add(section); } @@ -453,7 +453,7 @@ function collectStreamLayoutTargets( ): void { const smallOverlays = new Set(environment.queryAll('.streams-overlay.small')); for (const grid of environment.queryAll('.streams-grid')) { - const cards = grid.querySelectorAll('.stream-card'); + const cards = [...grid.querySelectorAll('.stream-card')]; const promotions = cards.filter(card => hiddenTargets.has(card) || isKrunkerStreamPromotionCard(card)); if (promotions.length === 0) continue; diff --git a/src/preload.ts b/src/preload.ts index 4c63f6d..a97d49f 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -608,11 +608,16 @@ function applyRawMouseInputPreference(_userPrefs: UserPrefs): void { */ function whenDOMReady(callback: () => void) { let ran = false; + // Declared before run() so every path that reaches run() sees initialized bindings. run() + // fires synchronously from the document.body branch below on every reload and on every + // did-finish-load fallback, where the body already exists. + let observer: MutationObserver | undefined; + let poll: number | undefined; const run = () => { if (ran) return; ran = true; observer?.disconnect(); - window.clearInterval(poll); + if (poll !== undefined) window.clearInterval(poll); document.removeEventListener('DOMContentLoaded', run); callback(); }; @@ -624,13 +629,13 @@ function whenDOMReady(callback: () => void) { document.addEventListener('DOMContentLoaded', run, { once: true }); // A parser that never emits the event still builds the body, so watch for the element itself. - const observer = typeof MutationObserver === 'function' + observer = typeof MutationObserver === 'function' ? new MutationObserver(() => { if (document.body) run(); }) : undefined; if (document.documentElement) observer?.observe(document.documentElement, { childList: true, subtree: true }); // Last resort for a document that produces neither: the steps are all body-dependent, so a // cheap poll is preferable to features that silently never start. - const poll = window.setInterval(() => { if (document.body) run(); }, 250); + poll = window.setInterval(() => { if (document.body) run(); }, 250); } /** Resolve an element by id as soon as the parser creates it, giving up at DOMContentLoaded. */