diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..3867a0f --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +npm run lint diff --git a/README.md b/README.md index 22ad129..f59e217 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index 7b6c4eb..795b5ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 49f70dd..a9a6830 100644 --- a/package.json +++ b/package.json @@ -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 ", - "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.3", + "description": "Cross-platform Krunker game client", + "main": "dist/main/index.js", + "homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client", + "author": "Krunker Civilian Client ", + "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" + } +} diff --git a/src/main/index.ts b/src/main/index.ts index 53b444f..a089e0a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -211,14 +211,14 @@ app.whenReady().then(async () => { async function launchApp(): Promise { 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 +240,17 @@ async function launchApp(): Promise { : [...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 +461,8 @@ async function launchApp(): Promise { // ── 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), diff --git a/src/main/swapper.ts b/src/main/swapper.ts index 2d19b88..025ca1c 100644 --- a/src/main/swapper.ts +++ b/src/main/swapper.ts @@ -1,27 +1,53 @@ 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, (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; + } + return net.fetch(`file://${filePath}`); }); } @@ -47,6 +73,13 @@ export class ResourceSwapper { this.ready = true; } + /** Rescan the swap directory to pick up added/removed/changed files */ + async rescan(): Promise { + 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 +103,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; } diff --git a/vite.preload.config.ts b/vite.preload.config.ts index 9aed58e..cc165f5 100644 --- a/vite.preload.config.ts +++ b/vite.preload.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ outDir: 'dist/preload', emptyDirBefore: true, rollupOptions: { - external: ['electron', 'uuid'], + external: ['electron'], }, target: 'node20', minify: isProd,