Compare commits

..

5 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
bigjakk 1eabea195a v0.5.2 — Add ESLint + typescript-eslint, fix lint errors
Add flat-config ESLint with typescript-eslint recommended rules,
fix all lint errors (unused imports/vars, empty catches, Function types,
prefer-const, useless assignments), and rename stale kpc- CSS class to kcc-.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:24:34 -08:00
18 changed files with 1140 additions and 137 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:all` | Build + package for all platforms |
| `npm run clean` | Remove `dist/` and `out/` directories |
| `npm run lint` | Run ESLint (typescript-eslint) on `src/` |
## Architecture
@@ -138,7 +139,7 @@ src/
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
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
@@ -159,7 +160,7 @@ 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. |
| `vite.preload.config.ts` | Preload script | `dist/preload/index.js` (CJS) | Externalizes `electron`. Targets Node 20. |
### Custom Electron Binary
@@ -214,7 +215,7 @@ Common flags always applied: `disable-backgrounding-occluded-windows`, `ignore-g
### 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)
- 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)
@@ -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
- **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)
- **ESLint** 10 + **typescript-eslint** — Linting with recommended rules
- **Husky** 9 — Git hooks (pre-commit lint)
## Project Structure
@@ -284,6 +286,7 @@ Krunker-Civilian-Client/
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
+1 -1
View File
@@ -25,7 +25,6 @@ files:
- node_modules/mimic-fn/**/*
- node_modules/semver/**/*
- node_modules/onetime/**/*
- node_modules/uuid/**/*
- "!node_modules/**/*.ts"
- "!node_modules/**/*.map"
asar: true
@@ -43,6 +42,7 @@ nsis:
allowToChangeInstallationDirectory: true
createDesktopShortcut: true
createStartMenuShortcut: true
shortcutName: Krunker Civilian Client
artifactName: "${productName}-${version}-Setup.${ext}"
portable:
+19
View File
@@ -0,0 +1,19 @@
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/", "out/", "scripts/"],
},
{
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
],
},
}
);
+929 -39
View File
File diff suppressed because it is too large Load Diff
+39 -34
View File
@@ -1,34 +1,39 @@
{
"name": "krunker-civilian-client",
"version": "0.5.1",
"description": "Cross-platform Krunker game client",
"main": "dist/main/index.js",
"homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client",
"author": "Krunker Civilian Client <krunker@crjlab.net>",
"license": "MIT",
"scripts": {
"postinstall": "node scripts/download-electron.js",
"dev": "vite build --mode development --config vite.main.config.ts && vite build --mode development --config vite.preload.config.ts && electron .",
"build:main": "vite build --config vite.main.config.ts",
"build:preload": "vite build --config vite.preload.config.ts",
"build": "npm run build:main && npm run build:preload",
"start": "npm run build && electron .",
"download-electron": "node scripts/download-electron.js",
"dist:win": "npm run build && electron-builder --win",
"dist:linux": "npm run build && electron-builder --linux",
"dist:all": "npm run build && electron-builder --win --linux",
"clean": "rimraf dist out"
},
"dependencies": {
"electron-store": "^8.2.0",
"uuid": "^9.0.1"
},
"devDependencies": {
"@types/node": "^22.0.0",
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
"electron-builder": "^26.0.0",
"rimraf": "^6.0.1",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
}
{
"name": "krunker-civilian-client",
"version": "0.5.6",
"description": "Cross-platform Krunker game client",
"main": "dist/main/index.js",
"homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client",
"author": "Krunker Civilian Client <krunker@crjlab.net>",
"license": "MIT",
"scripts": {
"postinstall": "node scripts/download-electron.js",
"dev": "vite build --mode development --config vite.main.config.ts && vite build --mode development --config vite.preload.config.ts && electron .",
"build:main": "vite build --config vite.main.config.ts",
"build:preload": "vite build --config vite.preload.config.ts",
"build": "npm run build:main && npm run build:preload",
"start": "npm run build && electron .",
"download-electron": "node scripts/download-electron.js",
"dist:win": "npm run build && electron-builder --win",
"dist:linux": "npm run build && electron-builder --linux",
"dist:all": "npm run build && electron-builder --win --linux",
"clean": "rimraf dist out",
"lint": "eslint src/",
"prepare": "husky"
},
"dependencies": {
"electron-store": "^8.2.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^22.0.0",
"electron": "npm:electron-nightly@42.0.0-nightly.20260227",
"electron-builder": "^26.0.0",
"eslint": "^10.0.2",
"husky": "^9.1.7",
"rimraf": "^6.0.1",
"typescript": "^5.7.0",
"typescript-eslint": "^8.56.1",
"vite": "^6.0.0"
}
}
+1 -1
View File
@@ -518,7 +518,7 @@ export const MATCHMAKER_SETTINGS_CSS = `
`;
export const TRANSLATOR_CSS = `
.kpc-translation {
.kcc-translation {
color: #88ff88;
font-style: italic;
margin-left: 8px;
+2
View File
@@ -45,6 +45,7 @@ export interface AppConfig {
maxPlayers: number;
minRemainingTime: number;
openServerBrowser: boolean;
autoJoin: boolean;
};
keybinds: {
reload: Keybind;
@@ -140,6 +141,7 @@ export const config = new Store<AppConfig>({
maxPlayers: 6,
minRemainingTime: 120,
openServerBrowser: true,
autoJoin: false,
},
keybinds: DEFAULT_KEYBINDS,
userscripts: {
+17 -12
View File
@@ -1,4 +1,4 @@
import { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, protocol, safeStorage, session, shell } from 'electron';
import { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, safeStorage, session, shell } from 'electron';
import { join } from 'path';
import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { get as httpsGet } from 'https';
@@ -14,6 +14,7 @@ import { showUpdateWindow } from './update-window';
import { DiscordRPC } from './discord-rpc';
// ── App version for API calls ──
// eslint-disable-next-line @typescript-eslint/no-require-imports
const appVersion: string = require('../../package.json').version;
// ── Region ping cache ──
@@ -53,6 +54,9 @@ const advancedConfig = { ...advancedDefaults, ...config.get('advanced') };
const perfConfig = { fpsUnlocked: true, ...config.get('performance') };
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) ──
initSwapperProtocol();
@@ -210,14 +214,14 @@ app.whenReady().then(async () => {
async function launchApp(): Promise<void> {
electronLog.log('[KCC] Starting initialization');
// ── Register swapper file protocol ──
registerSwapperFileProtocol();
// ── Session: persistent partition + clean user-agent ──
const ses = session.fromPartition('persist:krunker');
const rawUA = ses.getUserAgent();
ses.setUserAgent(rawUA.replace(/\s*krunker-civilian-client\/\S+/i, ''));
// ── Register swapper file protocol on this session ──
registerSwapperFileProtocol(ses);
// ── Resource swapper ──
const swapperConfig = config.get('swapper');
const swapDir = swapperConfig.path || join(app.getPath('userData'), 'Krunker Civilian Client', 'swapper');
@@ -239,16 +243,17 @@ async function launchApp(): Promise<void> {
: [...BLOCKED_URL_PATTERNS];
ses.webRequest.onBeforeRequest({ urls: requestFilterUrls }, (details, callback) => {
// Check swapper first — redirect matching assets to local files
if (swapper) {
const redirect = swapper.getRedirect(details.url);
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 {
const host = new URL(details.url).hostname;
if (host.endsWith('krunker.io')) return callback({});
} catch {}
// Otherwise it matched an ad-block pattern — cancel it
if (new URL(details.url).hostname.endsWith('krunker.io')) return callback({});
} catch { /* invalid URL — fall through to cancel */ }
// Matched an ad-block pattern — cancel it
callback({ cancel: true });
});
@@ -317,8 +322,6 @@ async function launchApp(): Promise<void> {
}
// ── Common output directory (used by folder actions) ──
const outputDir = join(app.getPath('documents'), 'Krunker Civilian Client');
// ── Configurable keybinds via before-input-event ──
win.webContents.on('before-input-event', (event, input) => {
if (input.type !== 'keyDown') return;
@@ -333,7 +336,7 @@ async function launchApp(): Promise<void> {
event.preventDefault();
} else if (matchesKeybind(input, binds.joinFromClipboard)) {
const text = clipboard.readText();
try { const u = new URL(text); if (u.protocol === 'https:' && u.hostname.endsWith('krunker.io')) win.loadURL(text); } catch {};
try { const u = new URL(text); if (u.protocol === 'https:' && u.hostname.endsWith('krunker.io')) win.loadURL(text); } catch { /* ignore invalid URLs */ }
event.preventDefault();
} else if (matchesKeybind(input, binds.copyGameLink)) {
clipboard.writeText(win.webContents.getURL());
@@ -461,6 +464,8 @@ async function launchApp(): Promise<void> {
// ── Inject scripts after page loads ──
win.webContents.on('did-finish-load', () => {
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([
win.webContents.insertCSS(HIDE_ADS_CSS),
win.webContents.insertCSS(ALL_CLIENT_CSS),
+1 -1
View File
@@ -67,7 +67,7 @@ function makeLogger(getStream: () => WriteStream) {
export const electronLog = makeLogger(() => electronStream);
export function getLogPath(type: 'electron'): string {
export function getLogPath(_type: 'electron'): string {
init();
return electronPath;
}
+46 -9
View File
@@ -1,27 +1,57 @@
import { existsSync, mkdirSync, promises as fsp } from 'fs';
import { join } from 'path';
import { protocol, net } from 'electron';
import { protocol, net, Session } from 'electron';
const PROTOCOL_NAME = 'kpc-swap';
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.
*/
export function initSwapperProtocol(): void {
protocol.registerSchemesAsPrivileged([{
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 {
protocol.handle(PROTOCOL_NAME, (request) => {
const filePath = decodeURI(request.url.replace(`${PROTOCOL_NAME}:`, ''));
return net.fetch('file://' + filePath);
export function registerSwapperFileProtocol(ses: Session): void {
ses.protocol.handle(PROTOCOL_NAME, async (request) => {
const url = new URL(request.url);
// 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;
}
/** 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 */
get patterns(): string[] {
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);
if (pathname.startsWith('/assets/')) pathname = pathname.substring(7);
const localPath = this.swapFiles.get(pathname);
if (localPath) return `${PROTOCOL_NAME}:/${localPath}`;
if (localPath) return filePathToSwapURL(localPath);
} catch { /* malformed URL — ignore */ }
return null;
}
@@ -87,7 +124,7 @@ export class ResourceSwapper {
this.swapFiles.set(name, join(this.swapDir, name));
}
}
} catch (err) {
} catch {
console.error(`Failed to scan swap directory prefix: ${prefix}`);
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { get as httpsGet, request as httpsRequest } from 'https';
import { get as httpsGet } from 'https';
import { createWriteStream, renameSync, unlinkSync, existsSync } from 'fs';
import { spawn } from 'child_process';
import { app } from 'electron';
+1 -1
View File
@@ -49,7 +49,7 @@ export class UserscriptManager {
/** Load tracker.json, add new scripts as disabled, prune deleted scripts */
async loadTracker(scripts: ScriptFile[]): Promise<ScriptTracker> {
let tracker: ScriptTracker = {};
let tracker: ScriptTracker;
try {
tracker = JSON.parse(await fsp.readFile(this.trackerPath, 'utf-8'));
} catch { tracker = {}; }
+15 -8
View File
@@ -2,7 +2,7 @@ import { ipcRenderer } from 'electron';
import { fetchGame, MATCHMAKER_GAMEMODES, MATCHMAKER_REGIONS, MATCHMAKER_REGION_NAMES } from './matchmaker';
import type { MatchmakerConfig } from './matchmaker';
import { initUserscripts, getInstances, setScriptEnabled } from './userscripts';
import type { UserscriptInstance, UserscriptSetting } from './userscripts';
import type { UserscriptInstance } from './userscripts';
import { initTranslator, updateTranslatorConfig } from './translator';
import { setDeathAnimBlock, escapeHtml } from './utils';
import type { Keybind } from '../main/config';
@@ -10,7 +10,7 @@ import type { Keybind } from '../main/config';
// ── Save console methods before Krunker overwrites them ──
// Wrapped to forward errors/warnings always, and logs when verbose is enabled
let _verboseLogging = false;
const _verboseLogging = false;
const _console = {
log: (...args: unknown[]) => {
@@ -75,7 +75,7 @@ function updateRefreshNotification(): void {
if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
return;
}
if (refreshPopupEl) { try { refreshPopupEl.remove(); } catch (_e) { /* noop */ } }
if (refreshPopupEl) { try { refreshPopupEl.remove(); } catch { /* noop */ } }
refreshPopupEl = document.createElement('div');
refreshPopupEl.className = 'kpc-holder-update refresh-popup';
if (refreshLevel === RefreshLevel.restart) {
@@ -590,7 +590,7 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any): 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 {
ipcRenderer.invoke('set-config', 'matchmaker', mm);
@@ -610,6 +610,13 @@ function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag
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) => {
bag.binds.matchmaker = b;
bag.saveBinds();
@@ -1115,7 +1122,7 @@ function renderUserscriptsSection(body: HTMLElement): void {
scriptRow.className = 'setting settName safety-0 bool';
const displayName = escapeHtml(inst.meta.name || inst.filename);
let metaParts: string[] = [];
const metaParts: string[] = [];
if (inst.meta.author) metaParts.push('by ' + escapeHtml(inst.meta.author));
if (inst.meta.version) metaParts.push('v' + escapeHtml(inst.meta.version));
const metaLine = metaParts.length > 0 ? '<span class="kpc-us-meta">' + metaParts.join(' &middot; ') + '</span>' : '';
@@ -1352,7 +1359,7 @@ ipcRenderer.on('main_did-finish-load', () => {
Promise.all([
ipcRenderer.invoke('get-all-config', ['ui', 'userscripts', 'game', 'translator', 'keybinds', 'discord']),
ipcRenderer.invoke('get-platform'),
]).then(([allConf, platformInfo]: [any, any]) => {
]).then(([allConf, _platformInfo]: [any, any]) => {
const uiConf = allConf.ui;
const usConf = allConf.userscripts;
const gameConf = allConf.game;
@@ -1417,7 +1424,7 @@ ipcRenderer.on('main_did-finish-load', () => {
let gameStartTimestamp = Math.floor(Date.now() / 1000);
function pollDiscordState(): void {
let details = '';
let details: string;
let state = '';
let startTimestamp: number | undefined = undefined;
@@ -1426,7 +1433,7 @@ ipcRenderer.on('main_did-finish-load', () => {
let gameActivity: any = null;
if (typeof w.getGameActivity === 'function') {
try { gameActivity = w.getGameActivity(); } catch {}
try { gameActivity = w.getGameActivity(); } catch { /* game API unavailable */ }
}
if (spectating) {
+55 -22
View File
@@ -1,7 +1,8 @@
// ── Custom Matchmaker (ported from Crankshaft) ──
// 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 { SavedConsole } from './utils';
@@ -28,6 +29,7 @@ export interface MatchmakerConfig {
maxPlayers: number;
minRemainingTime: number;
openServerBrowser: boolean;
autoJoin: boolean;
acceptKey: Keybind;
cancelKey: Keybind;
}
@@ -92,7 +94,7 @@ function getPopup(): PopupDOM {
}
// ── State ──
let currentMatch = '';
let popupGameID = '';
let openServerBrowser = true;
let confirmKey: Keybind = { key: 'Enter', 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;
if (typeof w.playSelect === 'function') w.playSelect();
if (accept && currentMatch !== 'none') {
window.location.href = `https://krunker.io/?game=${currentMatch}`;
if (accept && popupGameID !== 'none') {
window.location.href = `https://krunker.io/?game=${popupGameID}`;
} else {
const popup = getPopup();
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);
}
}
@@ -135,7 +137,7 @@ function createFetchedGamePopup(game: MatchmakerGame): void {
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)`;
currentMatch = game.gameID;
popupGameID = game.gameID;
if (game.gameID === 'none') {
popup.title.textContent = 'No Games Found...';
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);
}
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...');
async function fetchAndFilterGames(mmConfig: MatchmakerConfig): Promise<MatchmakerGame[]> {
const response = await fetch(`https://matchmaker.krunker.io/game-list?hostname=${window.location.hostname}`);
const result = await response.json();
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 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.gamemodes.length > 0 && !mmConfig.gamemodes.includes(gamemode)) continue;
if (playerCount < mmConfig.minPlayers) continue;
@@ -190,19 +182,60 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
if (remainingTime < mmConfig.minRemainingTime) continue;
if (playerCount === playerLimit) continue;
if (window.location.href.includes(gameID)) continue;
if (currentMatch === gameID) continue;
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) {
const selected = games[Math.floor(Math.random() * games.length)];
_con?.log('[KCC-MM] Selected:', selected.gameID, selected.region, selected.map);
createFetchedGamePopup(selected);
sortByPingThenPlayers(games, pings);
const best = games[0];
_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 {
_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({
gameID: 'none',
region: 'none',
+1
View File
@@ -138,6 +138,7 @@ const SYSTEM_PATTERNS = [
// ── Pre-translation filtering ──
function isLatinOnly(text: string): boolean {
// eslint-disable-next-line no-control-regex
return /^[\x00-\x7F\u00C0-\u024F\u1E00-\u1EFF\s\d.,!?;:'"()\-/@#$%^&*+=~`[\]{}|\\<>]+$/u.test(text);
}
+3 -3
View File
@@ -108,7 +108,7 @@ function toggleCSS(css: string, identifier: string, value: boolean): void {
function executeScript(
instance: UserscriptInstance,
_console: { log: Function; warn: Function; error: Function },
_console: { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
): void {
if (instance.executed) return;
@@ -164,7 +164,7 @@ export function getInstances(): UserscriptInstance[] {
}
export async function initUserscripts(
_console: { log: Function; warn: Function; error: Function },
_console: { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
): Promise<void> {
const { scripts, tracker } = await ipcRenderer.invoke('userscripts-scan');
if (!scripts || scripts.length === 0) {
@@ -219,7 +219,7 @@ export async function initUserscripts(
export function setScriptEnabled(
filename: string,
enabled: boolean,
_console: { log: Function; warn: Function; error: Function },
_console: { log: (...args: unknown[]) => void; warn: (...args: unknown[]) => void; error: (...args: unknown[]) => void },
): { needsReload: boolean } {
const inst = instances.find(i => i.filename === filename);
if (!inst) return { needsReload: false };
+1 -1
View File
@@ -13,7 +13,7 @@ export default defineConfig({
outDir: 'dist/preload',
emptyDirBefore: true,
rollupOptions: {
external: ['electron', 'uuid'],
external: ['electron'],
},
target: 'node20',
minify: isProd,