Files
Krunker-Civilian-Client-Test/README.md
T
bigjakk 467ac95b4e v0.5.3 — Fix resource swapper, add Husky, remove uuid
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>
2026-03-01 11:19:10 -08:00

296 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 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:
```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
```