Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 60d4d5bb47 | |||
| 8f3a74ddb4 | |||
| 93775cc36a | |||
| 467ac95b4e |
@@ -0,0 +1 @@
|
||||
npm run lint
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Generated
+21
-17
@@ -1,17 +1,16 @@
|
||||
{
|
||||
"name": "krunker-civilian-client",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "krunker-civilian-client",
|
||||
"version": "0.5.1",
|
||||
"version": "0.5.2",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-store": "^8.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
"electron-store": "^8.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
@@ -19,6 +18,7 @@
|
||||
"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",
|
||||
@@ -4674,6 +4674,22 @@
|
||||
"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": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
|
||||
@@ -5789,6 +5805,7 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -6889,19 +6906,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "1.10.1",
|
||||
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",
|
||||
|
||||
+39
-38
@@ -1,38 +1,39 @@
|
||||
{
|
||||
"name": "krunker-civilian-client",
|
||||
"version": "0.5.2",
|
||||
"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/"
|
||||
},
|
||||
"dependencies": {
|
||||
"electron-store": "^8.2.0",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"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",
|
||||
"rimraf": "^6.0.1",
|
||||
"typescript": "^5.7.0",
|
||||
"typescript-eslint": "^8.56.1",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
+14
-8
@@ -54,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();
|
||||
|
||||
@@ -211,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');
|
||||
@@ -240,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 { /* ignore invalid URLs */ }
|
||||
// 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 });
|
||||
});
|
||||
|
||||
@@ -460,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),
|
||||
|
||||
+45
-8
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+55
-22
@@ -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',
|
||||
|
||||
@@ -13,7 +13,7 @@ export default defineConfig({
|
||||
outDir: 'dist/preload',
|
||||
emptyDirBefore: true,
|
||||
rollupOptions: {
|
||||
external: ['electron', 'uuid'],
|
||||
external: ['electron'],
|
||||
},
|
||||
target: 'node20',
|
||||
minify: isProd,
|
||||
|
||||
Reference in New Issue
Block a user