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>
This commit is contained in:
2026-03-01 14:48:54 -08:00
parent 8f3a74ddb4
commit 60d4d5bb47
4 changed files with 66 additions and 24 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "krunker-civilian-client",
"version": "0.5.5",
"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: {
+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',