Compare commits

...

3 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
7 changed files with 76 additions and 27 deletions
+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:
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "krunker-civilian-client",
"version": "0.5.3",
"version": "0.5.6",
"description": "Cross-platform Krunker game client",
"main": "dist/main/index.js",
"homepage": "https://gitea.crjlab.net/bigjakk/krunker-civilian-client",
+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: {
+3
View File
@@ -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();
+6 -2
View File
@@ -35,7 +35,7 @@ export function initSwapperProtocol(): void {
* Must be called AFTER app.ready.
*/
export function registerSwapperFileProtocol(ses: Session): void {
ses.protocol.handle(PROTOCOL_NAME, (request) => {
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
@@ -47,7 +47,11 @@ export function registerSwapperFileProtocol(ses: Session): void {
} else {
filePath = url.pathname;
}
return net.fetch(`file://${filePath}`);
try {
return await net.fetch(`file://${filePath}`);
} catch {
return new Response('Not found', { status: 404 });
}
});
}
+8 -1
View File
@@ -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
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',