bigjakk 93775cc36a v0.5.4 — Fix taskbar pin persistence across updates
Set app.setAppUserModelId() to match electron-builder appId
(com.krunkercivilian.client) so Windows associates the running process
with the installed shortcut. Add explicit shortcutName to NSIS config
for stable shortcut identity across installs. Remove stale uuid entry
from electron-builder files list.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:38:51 -08:00

Krunker Civilian Client

Cross-platform Electron-based game client for Krunker.io. Loads the game directly in a BrowserWindow with GPU optimizations, unlimited FPS, persistent sessions, ad blocking, resource swapping, custom matchmaker, userscript engine, and Discord Rich Presence.

Quick Start

npm install
npm start        # Build all targets + launch Electron

For development with sourcemaps:

npm run dev      # Builds in dev mode + launches Electron

Scripts

Script Description
npm run dev Development build + launch
npm start Full build (main + preload) + launch
npm run build Build both Vite targets
npm run build:main Build main process only
npm run build:preload Build preload script only
npm run dist:win Build + package for Windows (NSIS installer + portable)
npm run dist:linux Build + package for Linux (AppImage + deb)
npm run dist:all Build + package for all platforms
npm run clean Remove dist/ and out/ directories
npm run lint Run ESLint (typescript-eslint) on src/

Architecture

Custom Patched Electron 42

This client uses a custom-patched Electron 42 (Chromium 134, Node 24). The --disable-frame-rate-limit Chromium flag — required for unlimited FPS — causes input starvation ("aim freeze") on Chromium 84+. At uncapped frame rates (300+ FPS), the compositor floods the main thread task queue and input events get delayed 50-300ms, then snap to catch up. The ImplLatencyRecovery/MainLatencyRecovery features mitigated this on Chromium 8793 but were removed in Chromium 94.

The Patch

A single-line change in Chromium's main thread scheduler (main_thread_scheduler_impl.cc) demotes input tasks from kHighestPriority to kNormalPriority, allowing the scheduler's built-in anti-starvation logic to fairly interleave input and compositor work:

 case MainThreadTaskQueue::QueueTraits::PrioritisationType::kInput:
-  return TaskPriority::kHighestPriority;
+  return TaskPriority::kNormalPriority;

Benchmarked via CDP Input.dispatchMouseEvent:

Metric Before After
p99 latency 97ms 34ms
Max latency 308ms 38ms
Events >50ms 8.6% 0%
Frames rendered baseline +21%
Mouse events processed baseline +9%

The full patch file, GN build args, and step-by-step build instructions are in electron-build/.

Binary Distribution

The patched binary is downloaded automatically during npm install via scripts/download-electron.js from a Gitea release asset (electron-patched tag). The electron npm dependency (npm:electron-nightly@42.0.0-nightly.20260227) provides TypeScript types and the CLI entry point, but the actual binary comes from the patched download.

FPS uncap flags (always applied when enabled):

disable-frame-rate-limit + disable-gpu-vsync + max-gum-fps=9999

Building from Source

Prerequisites: Windows 10/11 x64, ~100 GB disk, 16+ GB RAM, Visual Studio 2022 with C++ workload.

# 1. Install depot_tools and add to PATH
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

# 2. Check out Electron source (~40-60 GB)
mkdir C:\electron && cd C:\electron
gclient config --name "src/electron" --unmanaged https://github.com/nicedayzhu/electron.git@v42.0.0-nightly.20260227
gclient sync --with_branch_heads --with_tags

# 3. Apply the patch
cd src
git apply path/to/electron-build/input-priority-fix.patch

# 4. Configure and build (~2-4 hours for release)
gn gen out/Release
cp path/to/electron-build/args.release.gn out/Release/args.gn
gn gen out/Release
ninja -C out/Release electron

# 5. Create distributable zip
ninja -C out/Release electron:dist_zip

See electron-build/BUILD.md for detailed instructions, testing builds, and verification steps.

How the Game Loads

The game is loaded directly in the main BrowserWindow via win.loadURL('https://krunker.io'). This gives the game full GPU compositing, identical to Chrome.

Process Model

Main Process (src/main/index.ts)
  |
  |-- Creates BrowserWindow loading krunker.io directly
  |-- Manages session (persist:krunker), UA cleaning, permissions
  |-- Ad blocking + resource swapping via webRequest.onBeforeRequest
  |-- Configurable keybinds via before-input-event
  |-- Matchmaker IPC trigger (sends config to preload on hotkey)
  |-- CSS injection (ad hiding, client settings, matchmaker popup, keybind dialog)
  |-- File-based logging (electron.log in userData/logs/)
  |-- IPC handlers for window controls, config, devtools, userscripts
  |-- Userscript manager (filesystem scanning, tracker, preferences)
  |-- Auto-updater (Gitea releases API check, Setup.exe download + launch)
  |-- Discord Rich Presence via raw IPC socket
  |-- Consent dismiss via polling (no MutationObserver on main frame)
  |
  +-- Preload Script (src/preload/index.ts)
        |-- IPC bridge (window.kpc)
        |-- Action button grid (folder shortcuts, log viewers, reset/restart)
        |-- Settings UI hooks (collapsible sections: swapper, matchmaker, chat, translator, accounts, advanced, userscripts)
        |-- Userscript engine (metadata parser, execution, CSS injection, settings)
        |-- Matchmaker IPC listener -> fetchGame() in matchmaker.ts
        |-- Chat translator (Google Translate API, MutationObserver on #chatList)
        |-- Exit button (exposes Krunker's native #clientExit element)
        |-- Keybind capture dialog (Crankshaft-style)

Source Files

src/
  main/
    index.ts           Main process — window, IPC, session, ad blocking, keybinds
    platform.ts        OS detection, Chromium GPU/performance flags (per-platform)
    config.ts          electron-store schema, defaults, DEFAULT_KEYBINDS
    client-ui.ts       Injected CSS for settings panel, keybind dialog, matchmaker popup
    swapper.ts         Resource swapper — local asset overrides via session-aware custom protocol
    userscripts.ts     Userscript manager — filesystem scanning, tracker.json, preferences
    discord-rpc.ts     Discord Rich Presence via raw IPC socket
    logger.ts          File logging with daily rotation and 7-day retention
    updater.ts         Auto-updater — Gitea releases API check, download, installer launch
    update-window.ts   Update progress BrowserWindow with inline HTML + progress bar
  preload/
    index.ts           IPC bridge, settings UI hooks, keybind capture, matchmaker listener, alt manager
    utils.ts           Shared types (SavedConsole, KeybindDef), helpers (escapeHtml, genChatMsg)
    matchmaker.ts      Custom matchmaker — lobby fetch, filtering, popup UI
    translator.ts      Real-time chat translation via Google Translate API
    userscripts.ts     Userscript engine — metadata parser, execution, CSS injection, state

Build System

Two Vite configs build independent targets:

Config Target Output Notes
vite.main.config.ts Main process dist/main/index.js (CJS) Externalizes electron, electron-store, and Node builtins. Targets Node 20.
vite.preload.config.ts Preload script dist/preload/index.js (CJS) Externalizes electron. Targets Node 20.

Custom Electron Binary

The patched Electron binary is managed by scripts/download-electron.js:

  • Downloads from Gitea release tag electron-patched, asset electron-v42.0.0-nightly-patched-win32-x64.zip
  • Replaces node_modules/electron/dist/ with the patched binary
  • Runs automatically as a postinstall script during npm install
  • Skips download if the version file already matches

Configuration (electron-store)

Settings are persisted in krunker-civilian-config.json (OS-specific app data directory). Schema defined in src/main/config.ts:

interface AppConfig {
  window: { width, height, x, y, maximized, fullscreen }
  performance: { fpsUnlocked, hardwareAccel, gpuPreference }
  game: { lastServer, socialTabBehaviour, joinAsSpectator }
  swapper: { enabled, path }
  userscripts: { enabled, path }
  matchmaker: { enabled, regions, gamemodes, minPlayers, maxPlayers, minRemainingTime, openServerBrowser }
  keybinds: { reload, newMatch, copyGameLink, joinFromClipboard, devTools, matchmaker,
              matchmakerAccept, matchmakerCancel, pauseChat, fullscreenToggle }
  ui: { showExitButton, deathscreenAnimation, hideMenuPopups }
  discord: { enabled }
  translator: { enabled, targetLanguage, showLanguageTag }
  advanced: { removeUselessFeatures, gpuRasterizing, helpfulFlags, disableAccelerated2D, increaseLimits,
              lowLatency, experimentalFlags, angleBackend }
  accounts: SavedAccount[]
  platform: { detectedOS, gpuBackend }
}

Platform Handling

Feature Windows Linux
Window chrome Standard OS frame (frame: true) Standard OS frame (frame: true)
GPU backend D3D11 via ANGLE (default, configurable) Default (+ ozone-platform-hint auto for Wayland)
GPU sandbox Default Disabled (AppImage FUSE + Mesa driver compat)

Common flags always applied: disable-backgrounding-occluded-windows, ignore-gpu-blocklist. FPS uncap flags (disable-frame-rate-limit, disable-gpu-vsync, max-gum-fps=9999) applied when enabled. The ANGLE backend is configurable (default D3D11 on Windows; options include OpenGL, Vulkan, D3D9, D3D11on12). Additional Chromium flags (GPU rasterization, useful features, low latency, etc.) are configurable via the Advanced settings panel.

Session & User-Agent

  • Uses partition: 'persist:krunker' for persistent login/settings across restarts
  • User-Agent is cleaned to strip the app name identifier (keeps Electron/ so Krunker enables its Client settings tab)

Features

Core Client

  • Unlimited FPS via custom-patched Electron 42 build
  • Ad blocking (network-level URL filter + CSS hiding)
  • Resource swapper (replace game textures, sounds, models with local files — rescans on page refresh)
  • Custom matchmaker (filter lobbies by region, gamemode, player count, remaining time)
  • Userscript system (Tampermonkey-style metadata, custom per-script settings, instant toggle via unload)
  • Chat translator (real-time translation via Google Translate API with language tags)
  • Exit button in Krunker's sidebar menu (exposes native #clientExit element)
  • Selectable + pausable chat (always-on text selection, F10 to freeze auto-scroll for reading history)
  • Configurable keybinds with Crankshaft-style rebinding dialog
  • Configurable ANGLE backend (D3D11, OpenGL, Vulkan, D3D9, D3D11on12 — platform-filtered)
  • Advanced Chromium flag settings (GPU rasterization, low latency, experimental features, and more)
  • Client settings tab in Krunker's settings panel with collapsible sections
  • Action button grid in settings (folder shortcuts, log viewers, reset/restart options)
  • File-based logging (electron.log in userData/logs/, daily rotation with 7-day retention)
  • Persistent game session, window state persistence, cookie consent auto-dismiss
  • Auto-updater (Gitea releases API check, Setup.exe download with progress window, installer launch)
  • Discord Rich Presence (game state, map, mode, player count)
  • Death screen animation blocker (prevents CSS animation performance impact at uncapped FPS)
  • Menu popup hider (hides store ads, stream containers, news popups)

Alt Manager (Account Switching)

  • Account storage — Save multiple Krunker accounts (label, username, password)
  • Quick switch — In-game "Accounts" menu item opens Krunker's native popup with saved accounts
  • Login automation — Auto-logout + loginOrRegister() credential fill
  • Settings section — Full account CRUD in the collapsed "Accounts" settings section
  • Credential obfuscation — Credentials stored with character-shift encoding to prevent casual config reading

Known Constraints

MutationObserver Breaks WebGL on Main Frame

A MutationObserver with { childList: true, subtree: true } on Krunker's main frame DOM causes the WebGL 3D engine to hang indefinitely. Consent dismiss uses polling (setInterval) on the main frame instead. MutationObserver is safe in iframes and targeted elements like #menuWindow.

MutationObserver Infinite Loops (Chromium 94+)

Observers that modify elements within their observed subtree can cause infinite loops. In Chromium 94+, even textContent assignments that don't change the value still create new text nodes (triggering childList mutations). Observers must disconnect before DOM modifications and reconnect after.

Synthetic Events Lack User Activation (Chromium 94+)

Events created via new MouseEvent() / new KeyboardEvent() have isTrusted: false and do not provide user activation. Krunker ignores untrusted input events. The client uses webContents.sendInputEvent() via IPC to inject trusted input events from the main process.

Custom Electron Build Required

The --disable-frame-rate-limit flag causes compositor input starvation on all Chromium versions 84+. The ImplLatencyRecovery/MainLatencyRecovery features that mitigated this were removed in Chromium 94. Our custom Electron 42 build patches the Chromium main thread scheduler to fairly interleave input and compositor work. See Building from Source above and electron-build/BUILD.md for full instructions.

Uncapped FPS and CSS Animations

At uncapped frame rates (600+ FPS), Krunker's CSS animations (e.g. death screen slide-in) force a layout recalculation on every frame, which can saturate the renderer main thread. The death screen animation blocker (ui.deathscreenAnimation) is enabled by default to prevent this.

Tech Stack

  • Electron 42 (Chromium 134, Node 24) — Custom-patched build for unlimited FPS
  • TypeScript 5.7 — Type-safe source code
  • Vite 6 — Fast bundler (2 build targets: main + preload)
  • electron-store 8 — JSON config persistence (CJS)
  • electron-builder 26 — Cross-platform packaging (NSIS, portable, AppImage, deb)
  • ESLint 10 + typescript-eslint — Linting with recommended rules
  • Husky 9 — Git hooks (pre-commit lint)

Project Structure

Krunker-Civilian-Client/
  src/
    main/           Main process source (10 modules)
    preload/        Preload script source (5 modules)
  dist/             Vite build output
    main/
    preload/
  out/              electron-builder packaged output
  build/            Build resources (icons, .desktop file)
  scripts/          Build scripts (Electron patched binary download)
  electron-build/   Custom Electron build instructions and patches
  eslint.config.mjs
  vite.main.config.ts
  vite.preload.config.ts
  electron-builder.yml
  tsconfig.json
  package.json
S
Description
No description provided
Readme GPL-3.0 3.3 MiB
v0.8.0 Latest
2026-04-16 17:48:27 +00:00
Languages
TypeScript 97.7%
JavaScript 1.7%
Shell 0.6%