# Krunker Civilian Client

Cross-platform Electron-based game client for [Krunker.io](https://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

```bash
npm install
npm start        # Build all targets + launch Electron
```

For development with sourcemaps:

```bash
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 |

## 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 87–93 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:

```diff
 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/`](electron-build/BUILD.md).

#### 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.

```bash
# 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`](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 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` and `uuid`. 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`:

```typescript
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)
- 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](#building-from-source) above and [`electron-build/BUILD.md`](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)
- **uuid** 9 — Unique ID generation
- **electron-builder** 26 — Cross-platform packaging (NSIS, portable, AppImage, deb)

## 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
  vite.main.config.ts
  vite.preload.config.ts
  electron-builder.yml
  tsconfig.json
  package.json
```
