467ac95b4e
Fix three Electron 12→42 protocol migration bugs in the resource swapper: register protocol on the app session instead of default, generate valid URLs from Windows paths, and prevent non-swapped krunker.io requests from being cancelled. Swapper now rescans on page refresh to pick up file changes. Add Husky pre-commit hook to run ESLint. Remove unused uuid dependency. Update README with lint script, husky, and swapper improvements. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
296 lines
15 KiB
Markdown
296 lines
15 KiB
Markdown
# 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 |
|
||
| `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 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 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`:
|
||
|
||
```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 — 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](#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)
|
||
- **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
|
||
```
|