Compare commits

...

4 Commits

Author SHA1 Message Date
bigjakk 60d4d5bb47 v0.5.6 — Ping-sorted matchmaker with auto-join
Matchmaker now sorts filtered lobbies by lowest ping then highest
player count instead of picking randomly. New auto-join toggle skips
the popup and navigates directly to the best match.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:48:54 -08:00
bigjakk 8f3a74ddb4 v0.5.5 — Fix ERR_FILE_NOT_FOUND log spam in resource swapper
Wrap net.fetch() in the kpc-swap protocol handler with try/catch and
return a 404 response on failure instead of letting the error propagate
as an uncaught promise rejection. Revert workflow cache changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:29:52 -08:00
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
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
11 changed files with 194 additions and 100 deletions
+1
View File
@@ -0,0 +1 @@
npm run lint
+7 -4
View File
@@ -28,6 +28,7 @@ npm run dev # Builds in dev mode + launches Electron
| `npm run dist:linux` | Build + package for Linux (AppImage + deb) | | `npm run dist:linux` | Build + package for Linux (AppImage + deb) |
| `npm run dist:all` | Build + package for all platforms | | `npm run dist:all` | Build + package for all platforms |
| `npm run clean` | Remove `dist/` and `out/` directories | | `npm run clean` | Remove `dist/` and `out/` directories |
| `npm run lint` | Run ESLint (typescript-eslint) on `src/` |
## Architecture ## Architecture
@@ -138,7 +139,7 @@ src/
platform.ts OS detection, Chromium GPU/performance flags (per-platform) platform.ts OS detection, Chromium GPU/performance flags (per-platform)
config.ts electron-store schema, defaults, DEFAULT_KEYBINDS config.ts electron-store schema, defaults, DEFAULT_KEYBINDS
client-ui.ts Injected CSS for settings panel, keybind dialog, matchmaker popup client-ui.ts Injected CSS for settings panel, keybind dialog, matchmaker popup
swapper.ts Resource swapper — local asset overrides via custom protocol swapper.ts Resource swapper — local asset overrides via session-aware custom protocol
userscripts.ts Userscript manager — filesystem scanning, tracker.json, preferences userscripts.ts Userscript manager — filesystem scanning, tracker.json, preferences
discord-rpc.ts Discord Rich Presence via raw IPC socket discord-rpc.ts Discord Rich Presence via raw IPC socket
logger.ts File logging with daily rotation and 7-day retention logger.ts File logging with daily rotation and 7-day retention
@@ -159,7 +160,7 @@ Two Vite configs build independent targets:
| Config | Target | Output | Notes | | 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.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. | | `vite.preload.config.ts` | Preload script | `dist/preload/index.js` (CJS) | Externalizes `electron`. Targets Node 20. |
### Custom Electron Binary ### Custom Electron Binary
@@ -214,7 +215,7 @@ Common flags always applied: `disable-backgrounding-occluded-windows`, `ignore-g
### Core Client ### Core Client
- Unlimited FPS via custom-patched Electron 42 build - Unlimited FPS via custom-patched Electron 42 build
- Ad blocking (network-level URL filter + CSS hiding) - Ad blocking (network-level URL filter + CSS hiding)
- Resource swapper (replace game textures, sounds, models with local files) - 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) - Custom matchmaker (filter lobbies by region, gamemode, player count, remaining time)
- Userscript system (Tampermonkey-style metadata, custom per-script settings, instant toggle via unload) - 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) - Chat translator (real-time translation via Google Translate API with language tags)
@@ -267,8 +268,9 @@ At uncapped frame rates (600+ FPS), Krunker's CSS animations (e.g. death screen
- **TypeScript** 5.7 — Type-safe source code - **TypeScript** 5.7 — Type-safe source code
- **Vite** 6 — Fast bundler (2 build targets: main + preload) - **Vite** 6 — Fast bundler (2 build targets: main + preload)
- **electron-store** 8 — JSON config persistence (CJS) - **electron-store** 8 — JSON config persistence (CJS)
- **uuid** 9 — Unique ID generation
- **electron-builder** 26 — Cross-platform packaging (NSIS, portable, AppImage, deb) - **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 ## Project Structure
@@ -284,6 +286,7 @@ Krunker-Civilian-Client/
build/ Build resources (icons, .desktop file) build/ Build resources (icons, .desktop file)
scripts/ Build scripts (Electron patched binary download) scripts/ Build scripts (Electron patched binary download)
electron-build/ Custom Electron build instructions and patches electron-build/ Custom Electron build instructions and patches
eslint.config.mjs
vite.main.config.ts vite.main.config.ts
vite.preload.config.ts vite.preload.config.ts
electron-builder.yml electron-builder.yml
+1 -1
View File
@@ -25,7 +25,6 @@ files:
- node_modules/mimic-fn/**/* - node_modules/mimic-fn/**/*
- node_modules/semver/**/* - node_modules/semver/**/*
- node_modules/onetime/**/* - node_modules/onetime/**/*
- node_modules/uuid/**/*
- "!node_modules/**/*.ts" - "!node_modules/**/*.ts"
- "!node_modules/**/*.map" - "!node_modules/**/*.map"
asar: true asar: true
@@ -43,6 +42,7 @@ nsis:
allowToChangeInstallationDirectory: true allowToChangeInstallationDirectory: true
createDesktopShortcut: true createDesktopShortcut: true
createStartMenuShortcut: true createStartMenuShortcut: true
shortcutName: Krunker Civilian Client
artifactName: "${productName}-${version}-Setup.${ext}" artifactName: "${productName}-${version}-Setup.${ext}"
portable: portable:
+21 -17
View File
@@ -1,17 +1,16 @@
{ {
"name": "krunker-civilian-client", "name": "krunker-civilian-client",
"version": "0.5.1", "version": "0.5.2",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "krunker-civilian-client", "name": "krunker-civilian-client",
"version": "0.5.1", "version": "0.5.2",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"electron-store": "^8.2.0", "electron-store": "^8.2.0"
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -19,6 +18,7 @@
"electron": "npm:electron-nightly@42.0.0-nightly.20260227", "electron": "npm:electron-nightly@42.0.0-nightly.20260227",
"electron-builder": "^26.0.0", "electron-builder": "^26.0.0",
"eslint": "^10.0.2", "eslint": "^10.0.2",
"husky": "^9.1.7",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"typescript-eslint": "^8.56.1", "typescript-eslint": "^8.56.1",
@@ -4674,6 +4674,22 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/husky": {
"version": "9.1.7",
"resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz",
"integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==",
"dev": true,
"license": "MIT",
"bin": {
"husky": "bin.js"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/typicode"
}
},
"node_modules/iconv-corefoundation": { "node_modules/iconv-corefoundation": {
"version": "1.1.7", "version": "1.1.7",
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz", "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
@@ -5789,6 +5805,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
}, },
@@ -6889,19 +6906,6 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
}
},
"node_modules/verror": { "node_modules/verror": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
+5 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "krunker-civilian-client", "name": "krunker-civilian-client",
"version": "0.5.2", "version": "0.5.6",
"description": "Cross-platform Krunker game client", "description": "Cross-platform Krunker game client",
"main": "dist/main/index.js", "main": "dist/main/index.js",
"homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client", "homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client",
@@ -18,11 +18,11 @@
"dist:linux": "npm run build && electron-builder --linux", "dist:linux": "npm run build && electron-builder --linux",
"dist:all": "npm run build && electron-builder --win --linux", "dist:all": "npm run build && electron-builder --win --linux",
"clean": "rimraf dist out", "clean": "rimraf dist out",
"lint": "eslint src/" "lint": "eslint src/",
"prepare": "husky"
}, },
"dependencies": { "dependencies": {
"electron-store": "^8.2.0", "electron-store": "^8.2.0"
"uuid": "^9.0.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^10.0.1", "@eslint/js": "^10.0.1",
@@ -30,6 +30,7 @@
"electron": "npm:electron-nightly@42.0.0-nightly.20260227", "electron": "npm:electron-nightly@42.0.0-nightly.20260227",
"electron-builder": "^26.0.0", "electron-builder": "^26.0.0",
"eslint": "^10.0.2", "eslint": "^10.0.2",
"husky": "^9.1.7",
"rimraf": "^6.0.1", "rimraf": "^6.0.1",
"typescript": "^5.7.0", "typescript": "^5.7.0",
"typescript-eslint": "^8.56.1", "typescript-eslint": "^8.56.1",
+2
View File
@@ -45,6 +45,7 @@ export interface AppConfig {
maxPlayers: number; maxPlayers: number;
minRemainingTime: number; minRemainingTime: number;
openServerBrowser: boolean; openServerBrowser: boolean;
autoJoin: boolean;
}; };
keybinds: { keybinds: {
reload: Keybind; reload: Keybind;
@@ -140,6 +141,7 @@ export const config = new Store<AppConfig>({
maxPlayers: 6, maxPlayers: 6,
minRemainingTime: 120, minRemainingTime: 120,
openServerBrowser: true, openServerBrowser: true,
autoJoin: false,
}, },
keybinds: DEFAULT_KEYBINDS, keybinds: DEFAULT_KEYBINDS,
userscripts: { userscripts: {
+14 -8
View File
@@ -54,6 +54,9 @@ const advancedConfig = { ...advancedDefaults, ...config.get('advanced') };
const perfConfig = { fpsUnlocked: true, ...config.get('performance') }; const perfConfig = { fpsUnlocked: true, ...config.get('performance') };
applyPlatformFlags(platformInfo, advancedConfig, perfConfig); applyPlatformFlags(platformInfo, advancedConfig, perfConfig);
// ── App identity (must match electron-builder appId for taskbar pin persistence) ──
app.setAppUserModelId('com.krunkercivilian.client');
// ── Resource swapper protocol (must register before app.ready) ── // ── Resource swapper protocol (must register before app.ready) ──
initSwapperProtocol(); initSwapperProtocol();
@@ -211,14 +214,14 @@ app.whenReady().then(async () => {
async function launchApp(): Promise<void> { async function launchApp(): Promise<void> {
electronLog.log('[KCC] Starting initialization'); electronLog.log('[KCC] Starting initialization');
// ── Register swapper file protocol ──
registerSwapperFileProtocol();
// ── Session: persistent partition + clean user-agent ── // ── Session: persistent partition + clean user-agent ──
const ses = session.fromPartition('persist:krunker'); const ses = session.fromPartition('persist:krunker');
const rawUA = ses.getUserAgent(); const rawUA = ses.getUserAgent();
ses.setUserAgent(rawUA.replace(/\s*krunker-civilian-client\/\S+/i, '')); ses.setUserAgent(rawUA.replace(/\s*krunker-civilian-client\/\S+/i, ''));
// ── Register swapper file protocol on this session ──
registerSwapperFileProtocol(ses);
// ── Resource swapper ── // ── Resource swapper ──
const swapperConfig = config.get('swapper'); const swapperConfig = config.get('swapper');
const swapDir = swapperConfig.path || join(app.getPath('userData'), 'Krunker Civilian Client', 'swapper'); const swapDir = swapperConfig.path || join(app.getPath('userData'), 'Krunker Civilian Client', 'swapper');
@@ -240,16 +243,17 @@ async function launchApp(): Promise<void> {
: [...BLOCKED_URL_PATTERNS]; : [...BLOCKED_URL_PATTERNS];
ses.webRequest.onBeforeRequest({ urls: requestFilterUrls }, (details, callback) => { ses.webRequest.onBeforeRequest({ urls: requestFilterUrls }, (details, callback) => {
// Check swapper first — redirect matching assets to local files
if (swapper) { if (swapper) {
const redirect = swapper.getRedirect(details.url); const redirect = swapper.getRedirect(details.url);
if (redirect) return callback({ redirectURL: redirect }); if (redirect) return callback({ redirectURL: redirect });
} }
// If we got here via the broad krunker.io pattern (not an ad), let it through // Determine if this URL is a krunker.io request (matched by the broad swapper pattern)
// vs an ad-block pattern. krunker.io requests that weren't swapped pass through normally.
try { try {
const host = new URL(details.url).hostname; if (new URL(details.url).hostname.endsWith('krunker.io')) return callback({});
if (host.endsWith('krunker.io')) return callback({}); } catch { /* invalid URL — fall through to cancel */ }
} catch { /* ignore invalid URLs */ } // Matched an ad-block pattern — cancel it
// Otherwise it matched an ad-block pattern — cancel it
callback({ cancel: true }); callback({ cancel: true });
}); });
@@ -460,6 +464,8 @@ async function launchApp(): Promise<void> {
// ── Inject scripts after page loads ── // ── Inject scripts after page loads ──
win.webContents.on('did-finish-load', () => { win.webContents.on('did-finish-load', () => {
electronLog.log(`[KCC] Page loaded: ${win.webContents.getURL()}`); electronLog.log(`[KCC] Page loaded: ${win.webContents.getURL()}`);
// Rescan swap directory so new/changed files are picked up on refresh
if (swapper) swapper.rescan().catch(() => {});
Promise.all([ Promise.all([
win.webContents.insertCSS(HIDE_ADS_CSS), win.webContents.insertCSS(HIDE_ADS_CSS),
win.webContents.insertCSS(ALL_CLIENT_CSS), win.webContents.insertCSS(ALL_CLIENT_CSS),
+45 -8
View File
@@ -1,27 +1,57 @@
import { existsSync, mkdirSync, promises as fsp } from 'fs'; import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { join } from 'path'; import { join } from 'path';
import { protocol, net } from 'electron'; import { protocol, net, Session } from 'electron';
const PROTOCOL_NAME = 'kpc-swap'; const PROTOCOL_NAME = 'kpc-swap';
const TARGET_DOMAIN = 'krunker.io'; const TARGET_DOMAIN = 'krunker.io';
/**
* Convert a native file path to a proper kpc-swap:// URL.
* Windows paths like C:\foo\bar become kpc-swap://C/foo/bar
*/
function filePathToSwapURL(filePath: string): string {
const forwardSlash = filePath.replace(/\\/g, '/');
// Windows drive letter: C:/foo → kpc-swap://C/foo
const match = forwardSlash.match(/^([A-Za-z]):\/(.*)/);
if (match) {
return `${PROTOCOL_NAME}://${match[1]}/${match[2]}`;
}
// Unix absolute: /home/user/foo → kpc-swap:///home/user/foo
return `${PROTOCOL_NAME}://${forwardSlash}`;
}
/** /**
* Register the custom protocol scheme. Must be called BEFORE app.ready. * Register the custom protocol scheme. Must be called BEFORE app.ready.
*/ */
export function initSwapperProtocol(): void { export function initSwapperProtocol(): void {
protocol.registerSchemesAsPrivileged([{ protocol.registerSchemesAsPrivileged([{
scheme: PROTOCOL_NAME, scheme: PROTOCOL_NAME,
privileges: { secure: true, corsEnabled: true, bypassCSP: true }, privileges: { standard: true, secure: true, corsEnabled: true, bypassCSP: true },
}]); }]);
} }
/** /**
* Register the file protocol handler. Must be called AFTER app.ready. * Register the file protocol handler on the given session.
* Must be called AFTER app.ready.
*/ */
export function registerSwapperFileProtocol(): void { export function registerSwapperFileProtocol(ses: Session): void {
protocol.handle(PROTOCOL_NAME, (request) => { ses.protocol.handle(PROTOCOL_NAME, async (request) => {
const filePath = decodeURI(request.url.replace(`${PROTOCOL_NAME}:`, '')); const url = new URL(request.url);
return net.fetch('file://' + filePath); // Reconstruct the file path from the URL
// Windows: kpc-swap://C/foo/bar → C:/foo/bar
// Unix: kpc-swap:///home/foo → /home/foo
let filePath: string;
if (url.hostname) {
// Windows drive letter is the hostname
filePath = `${url.hostname}:${url.pathname}`;
} else {
filePath = url.pathname;
}
try {
return await net.fetch(`file://${filePath}`);
} catch {
return new Response('Not found', { status: 404 });
}
}); });
} }
@@ -47,6 +77,13 @@ export class ResourceSwapper {
this.ready = true; this.ready = true;
} }
/** Rescan the swap directory to pick up added/removed/changed files */
async rescan(): Promise<void> {
this.swapFiles.clear();
await this.scanAsync('');
this.ready = true;
}
/** URL filter patterns for webRequest.onBeforeRequest — single broad pattern */ /** URL filter patterns for webRequest.onBeforeRequest — single broad pattern */
get patterns(): string[] { get patterns(): string[] {
return this.swapFiles.size > 0 ? [`*://*.${TARGET_DOMAIN}/*`] : []; return this.swapFiles.size > 0 ? [`*://*.${TARGET_DOMAIN}/*`] : [];
@@ -70,7 +107,7 @@ export class ResourceSwapper {
let pathname = queryStart === -1 ? url.substring(pathStart) : url.substring(pathStart, queryStart); let pathname = queryStart === -1 ? url.substring(pathStart) : url.substring(pathStart, queryStart);
if (pathname.startsWith('/assets/')) pathname = pathname.substring(7); if (pathname.startsWith('/assets/')) pathname = pathname.substring(7);
const localPath = this.swapFiles.get(pathname); const localPath = this.swapFiles.get(pathname);
if (localPath) return `${PROTOCOL_NAME}:/${localPath}`; if (localPath) return filePathToSwapURL(localPath);
} catch { /* malformed URL — ignore */ } } catch { /* malformed URL — ignore */ }
return null; return null;
} }
+8 -1
View File
@@ -590,7 +590,7 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any): void {
} }
function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void { function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true }; const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, autoJoin: false };
function saveMM(): void { function saveMM(): void {
ipcRenderer.invoke('set-config', 'matchmaker', mm); ipcRenderer.invoke('set-config', 'matchmaker', mm);
@@ -610,6 +610,13 @@ function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag
onChange: (v) => { mm.openServerBrowser = v; saveMM(); }, onChange: (v) => { mm.openServerBrowser = v; saveMM(); },
})); }));
body.appendChild(createToggleRow({
label: 'Auto-Join',
desc: 'Automatically join the best match without showing the popup',
checked: mm.autoJoin ?? false, instant: true,
onChange: (v) => { mm.autoJoin = v; saveMM(); },
}));
body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => { body.appendChild(createKeybindRow('Matchmaker Hotkey', 'Key to trigger the custom matchmaker', bag.binds.matchmaker, (b) => {
bag.binds.matchmaker = b; bag.binds.matchmaker = b;
bag.saveBinds(); bag.saveBinds();
+55 -22
View File
@@ -1,7 +1,8 @@
// ── Custom Matchmaker (ported from Crankshaft) ── // ── Custom Matchmaker (ported from Crankshaft) ──
// Fetches live lobby list from matchmaker.krunker.io, filters by user criteria, // Fetches live lobby list from matchmaker.krunker.io, filters by user criteria,
// presents a popup to join a random matching game. // sorts by lowest ping then highest player count, and joins the best match.
import { ipcRenderer } from 'electron';
import type { Keybind } from '../main/config'; import type { Keybind } from '../main/config';
import type { SavedConsole } from './utils'; import type { SavedConsole } from './utils';
@@ -28,6 +29,7 @@ export interface MatchmakerConfig {
maxPlayers: number; maxPlayers: number;
minRemainingTime: number; minRemainingTime: number;
openServerBrowser: boolean; openServerBrowser: boolean;
autoJoin: boolean;
acceptKey: Keybind; acceptKey: Keybind;
cancelKey: Keybind; cancelKey: Keybind;
} }
@@ -92,7 +94,7 @@ function getPopup(): PopupDOM {
} }
// ── State ── // ── State ──
let currentMatch = ''; let popupGameID = '';
let openServerBrowser = true; let openServerBrowser = true;
let confirmKey: Keybind = { key: 'Enter', ctrl: false, shift: false, alt: false }; let confirmKey: Keybind = { key: 'Enter', ctrl: false, shift: false, alt: false };
let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false }; let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false };
@@ -101,12 +103,12 @@ function decideMatchmakerDecision(accept: boolean): void {
const w = window as any; const w = window as any;
if (typeof w.playSelect === 'function') w.playSelect(); if (typeof w.playSelect === 'function') w.playSelect();
if (accept && currentMatch !== 'none') { if (accept && popupGameID !== 'none') {
window.location.href = `https://krunker.io/?game=${currentMatch}`; window.location.href = `https://krunker.io/?game=${popupGameID}`;
} else { } else {
const popup = getPopup(); const popup = getPopup();
if (popup.element.parentNode) popup.element.remove(); if (popup.element.parentNode) popup.element.remove();
if (currentMatch === 'none' && openServerBrowser && typeof w.openServerWindow === 'function') { if (popupGameID === 'none' && openServerBrowser && typeof w.openServerWindow === 'function') {
w.openServerWindow(0); w.openServerWindow(0);
} }
} }
@@ -135,7 +137,7 @@ function createFetchedGamePopup(game: MatchmakerGame): void {
const mapIdx = MAP_ICON_INDICES.indexOf(game.map); const mapIdx = MAP_ICON_INDICES.indexOf(game.map);
popup.element.style.backgroundImage = `url(https://assets.krunker.io/img/maps/map_${mapIdx >= 0 ? mapIdx : 0}.png)`; popup.element.style.backgroundImage = `url(https://assets.krunker.io/img/maps/map_${mapIdx >= 0 ? mapIdx : 0}.png)`;
currentMatch = game.gameID; popupGameID = game.gameID;
if (game.gameID === 'none') { if (game.gameID === 'none') {
popup.title.textContent = 'No Games Found...'; popup.title.textContent = 'No Games Found...';
popup.description.textContent = 'Check the server browser to see other lobbies.'; popup.description.textContent = 'Check the server browser to see other lobbies.';
@@ -159,16 +161,7 @@ function createFetchedGamePopup(game: MatchmakerGame): void {
if (uiBase) uiBase.appendChild(popup.element); if (uiBase) uiBase.appendChild(popup.element);
} }
export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise<void> { async function fetchAndFilterGames(mmConfig: MatchmakerConfig): Promise<MatchmakerGame[]> {
openServerBrowser = mmConfig.openServerBrowser;
confirmKey = mmConfig.acceptKey;
cancelKey = mmConfig.cancelKey;
// Dismiss existing popup if active
if (document.getElementById(POPUP_ID)) decideMatchmakerDecision(false);
_con?.log('[KCC-MM] Fetching game list...');
const response = await fetch(`https://matchmaker.krunker.io/game-list?hostname=${window.location.hostname}`); const response = await fetch(`https://matchmaker.krunker.io/game-list?hostname=${window.location.hostname}`);
const result = await response.json(); const result = await response.json();
const games: MatchmakerGame[] = []; const games: MatchmakerGame[] = [];
@@ -182,7 +175,6 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
const gamemode = MATCHMAKER_GAMEMODES[game[4].g] ?? 'Unknown Gamemode'; const gamemode = MATCHMAKER_GAMEMODES[game[4].g] ?? 'Unknown Gamemode';
const remainingTime: number = game[5]; const remainingTime: number = game[5];
// Apply filters — empty arrays mean "all selected" (no filter)
if (mmConfig.regions.length > 0 && !mmConfig.regions.includes(region)) continue; if (mmConfig.regions.length > 0 && !mmConfig.regions.includes(region)) continue;
if (mmConfig.gamemodes.length > 0 && !mmConfig.gamemodes.includes(gamemode)) continue; if (mmConfig.gamemodes.length > 0 && !mmConfig.gamemodes.includes(gamemode)) continue;
if (playerCount < mmConfig.minPlayers) continue; if (playerCount < mmConfig.minPlayers) continue;
@@ -190,19 +182,60 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
if (remainingTime < mmConfig.minRemainingTime) continue; if (remainingTime < mmConfig.minRemainingTime) continue;
if (playerCount === playerLimit) continue; if (playerCount === playerLimit) continue;
if (window.location.href.includes(gameID)) continue; if (window.location.href.includes(gameID)) continue;
if (currentMatch === gameID) continue;
games.push({ gameID, region, playerCount, playerLimit, map, gamemode, remainingTime }); games.push({ gameID, region, playerCount, playerLimit, map, gamemode, remainingTime });
} }
_con?.log('[KCC-MM] Received', result.games?.length ?? 0, 'games,', games.length, 'passed filters'); return games;
}
function sortByPingThenPlayers(games: MatchmakerGame[], pings: Record<string, number>): MatchmakerGame[] {
return games.sort((a, b) => {
const pingA = pings[a.region] ?? 999;
const pingB = pings[b.region] ?? 999;
if (pingA !== pingB) return pingA - pingB;
return b.playerCount - a.playerCount;
});
}
export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise<void> {
openServerBrowser = mmConfig.openServerBrowser;
confirmKey = mmConfig.acceptKey;
cancelKey = mmConfig.cancelKey;
// Dismiss existing popup if active
if (document.getElementById(POPUP_ID)) decideMatchmakerDecision(false);
_con?.log('[KCC-MM] Fetching game list + pings...');
const [games, pings] = await Promise.all([
fetchAndFilterGames(mmConfig),
ipcRenderer.invoke('ping-regions').catch(() => ({} as Record<string, number>)),
]);
_con?.log('[KCC-MM]', games.length, 'games passed filters, pings:', pings);
if (games.length > 0) { if (games.length > 0) {
const selected = games[Math.floor(Math.random() * games.length)]; sortByPingThenPlayers(games, pings);
_con?.log('[KCC-MM] Selected:', selected.gameID, selected.region, selected.map); const best = games[0];
createFetchedGamePopup(selected); _con?.log('[KCC-MM] Best match:', best.gameID, best.region, best.map, `(${pings[best.region] ?? '?'}ms)`);
if (mmConfig.autoJoin) {
window.location.href = `https://krunker.io/?game=${best.gameID}`;
return;
}
createFetchedGamePopup(best);
} else { } else {
_con?.log('[KCC-MM] No matching games found'); _con?.log('[KCC-MM] No matching games found');
if (mmConfig.autoJoin) {
if (openServerBrowser && typeof (window as any).openServerWindow === 'function') {
(window as any).openServerWindow(0);
}
return;
}
createFetchedGamePopup({ createFetchedGamePopup({
gameID: 'none', gameID: 'none',
region: 'none', region: 'none',
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
outDir: 'dist/preload', outDir: 'dist/preload',
emptyDirBefore: true, emptyDirBefore: true,
rollupOptions: { rollupOptions: {
external: ['electron', 'uuid'], external: ['electron'],
}, },
target: 'node20', target: 'node20',
minify: isProd, minify: isProd,