From 60d4d5bb47ee35a34d26c80326271e07e1553bcc Mon Sep 17 00:00:00 2001 From: bigjakk Date: Sun, 1 Mar 2026 14:48:54 -0800 Subject: [PATCH] =?UTF-8?q?v0.5.6=20=E2=80=94=20Ping-sorted=20matchmaker?= =?UTF-8?q?=20with=20auto-join?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- package.json | 2 +- src/main/config.ts | 2 + src/preload/index.ts | 9 ++++- src/preload/matchmaker.ts | 77 ++++++++++++++++++++++++++++----------- 4 files changed, 66 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index f339fea..85b3ade 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/main/config.ts b/src/main/config.ts index bf402ce..531df8a 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -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({ maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, + autoJoin: false, }, keybinds: DEFAULT_KEYBINDS, userscripts: { diff --git a/src/preload/index.ts b/src/preload/index.ts index 2a18cb7..380e727 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -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(); diff --git a/src/preload/matchmaker.ts b/src/preload/matchmaker.ts index 8c77be8..1d35c23 100644 --- a/src/preload/matchmaker.ts +++ b/src/preload/matchmaker.ts @@ -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 { - 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 { 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): 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 { + 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)), + ]); + + _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',