;
- saveBinds: () => void;
- isWindows: boolean;
-}
-
-function buildGeneralSection(
- body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag,
-): void {
- const perfDefaults = { fpsUnlocked: true };
- const perf = { ...perfDefaults, ...perfConf };
-
- body.appendChild(createToggleRow({
- label: 'Unlimited FPS',
- desc: 'Uncap the frame rate (requires restart)',
- checked: perf.fpsUnlocked, restart: true,
- onChange: (v) => { perf.fpsUnlocked = v; ipcRenderer.invoke('set-config', 'performance', perf); },
- }));
-
- const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' };
- const game = { ...gameDefaults, ...gameConf };
-
- body.appendChild(createSelectRow({
- label: 'Social/Hub Tab Behaviour',
- desc: 'How social, market, and editor pages open when clicked',
- options: [{ value: 'New Window', label: 'Tabs (Separate Window)' }, { value: 'Same Window', label: 'Tabs (Overlay Game)' }],
- value: game.socialTabBehaviour, instant: true,
- onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
- }));
-
- const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
- const ui = { ...uiDefaults, ...uiConfRaw };
-
- function saveUI(): void {
- ipcRenderer.invoke('set-config', 'ui', ui);
- }
-
- body.appendChild(createToggleRow({
- label: 'Show Exit Button',
- desc: 'Show the exit button in the game sidebar',
- checked: ui.showExitButton, instant: true,
- onChange: (v) => {
- ui.showExitButton = v; saveUI();
- const btn = document.getElementById('clientExit');
- if (btn) btn.style.display = v ? 'flex' : 'none';
- },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Block Death Screen Animation',
- desc: 'Disable the slide-in animation on the death screen',
- checked: ui.deathscreenAnimation, instant: true,
- onChange: (v) => { ui.deathscreenAnimation = v; saveUI(); setDeathAnimBlock(v); },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Hide Menu Popups',
- desc: 'Hide promotional notifications, offers, and streams on the main menu',
- checked: ui.hideMenuPopups, instant: true,
- onChange: (v) => {
- ui.hideMenuPopups = v; saveUI();
- if (v) startHidePopups(); else stopHidePopups();
- },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Join as Spectator',
- desc: 'Automatically enable spectate mode when joining a game',
- checked: game.joinAsSpectator, instant: true,
- onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Menu Timer',
- desc: 'Show the game/spectate timer on the menu screen',
- checked: ui.menuTimer ?? true, instant: true,
- onChange: (v) => { ui.menuTimer = v; saveUI(); setMenuTimer(v); },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Double Ping Display',
- desc: 'Show the real ping value (Krunker displays half the actual latency)',
- checked: ui.doublePing ?? true, refreshOnly: true,
- onChange: (v) => { ui.doublePing = v; saveUI(); },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Show Ping in Player List',
- desc: 'Replace the ping icon with numeric millisecond values in the player list',
- checked: game.showPing ?? true, refreshOnly: true,
- onChange: (v) => { game.showPing = v; ipcRenderer.invoke('set-config', 'game', game); },
- }));
-
- if (bag.isWindows) {
- body.appendChild(createToggleRow({
- label: 'Raw Input',
- desc: 'Bypass OS mouse acceleration for direct 1:1 sensor input (Windows only)',
- checked: game.rawInput ?? true, refreshOnly: true,
- onChange: (v) => { game.rawInput = v; ipcRenderer.invoke('set-config', 'game', game); },
- }));
- }
-
- body.appendChild(createToggleRow({
- label: 'Hardpoint Enemy Counter',
- desc: 'Show enemy capture points in Hardpoint mode',
- checked: game.hpEnemyCounter ?? true, refreshOnly: true,
- onChange: (v) => {
- game.hpEnemyCounter = v; ipcRenderer.invoke('set-config', 'game', game);
- if (v) initHPCounter(); else destroyHPCounter();
- },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Show Changelog',
- desc: 'Show release notes popup when the client updates',
- checked: ui.showChangelog ?? true, instant: true,
- onChange: (v) => { ui.showChangelog = v; saveUI(); },
- }));
-
- if (ui.deathscreenAnimation) setDeathAnimBlock(true);
- if (ui.menuTimer ?? true) setMenuTimer(true);
- if (ui.hideMenuPopups) startHidePopups();
-
- body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
- bag.binds.fullscreenToggle = b;
- bag.saveBinds();
- }, undefined, true));
-}
-
-function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any): void {
- const swapEnabled = swapperConf ? swapperConf.enabled : true;
- const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
-
- function saveUI(): void {
- ipcRenderer.invoke('set-config', 'ui', ui);
- }
-
- body.appendChild(createToggleRow({
- label: 'Resource Swapper',
- desc: 'Replace game textures, sounds, and models with local files',
- checked: swapEnabled,
- restart: true,
- onChange: (v) => {
- ipcRenderer.invoke('get-config', 'swapper').then((conf: any) => {
- ipcRenderer.invoke('set-config', 'swapper', { enabled: v, path: conf ? conf.path : '' });
- });
- },
- }));
-
- const folderRow = document.createElement('div');
- folderRow.className = 'setting settName safety-0 has-button';
- folderRow.innerHTML =
- 'Swapper Folder' +
- 'Place replacement assets here (textures/, sound/, models/)
';
- const swapFolderBtn = document.createElement('div');
- swapFolderBtn.className = 'settingsBtn';
- swapFolderBtn.title = 'Open Folder';
- swapFolderBtn.innerHTML = 'folder Swapper';
- swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
- folderRow.appendChild(swapFolderBtn);
- body.appendChild(folderRow);
-
- // ── CSS Theme selector (populated from swap/themes/) ──
- const themeRow = document.createElement('div');
- themeRow.className = 'setting settName safety-0 sel has-button';
- themeRow.innerHTML =
- 'CSS Theme' +
- 'Load a custom CSS theme from swap/themes/
';
- const themeSelect = document.createElement('select');
- themeSelect.className = 's-update inputGrey2';
- themeSelect.innerHTML = '';
- themeRow.appendChild(themeSelect);
- const themeFolderBtn = document.createElement('div');
- themeFolderBtn.className = 'settingsBtn';
- themeFolderBtn.title = 'Open Themes Folder';
- themeFolderBtn.innerHTML = 'folder';
- themeFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-themes-folder'));
- themeRow.appendChild(themeFolderBtn);
- body.appendChild(themeRow);
-
- ipcRenderer.invoke('list-themes').then((themes: Array<{ id: string; label: string }>) => {
- themeSelect.innerHTML = '';
- for (const t of themes) {
- const opt = document.createElement('option');
- opt.value = t.id;
- opt.textContent = t.label;
- if (t.id === ui.cssTheme) opt.selected = true;
- themeSelect.appendChild(opt);
- }
- });
-
- themeSelect.addEventListener('change', () => {
- ui.cssTheme = themeSelect.value;
- saveUI();
- onSettingChanged('refresh');
- });
-
- // ── Loading Screen Background ──
- const bgRow = document.createElement('div');
- bgRow.className = 'setting settName safety-0 sel has-button';
- bgRow.innerHTML =
- 'Loading Background' +
- 'Custom background image for the loading screen (swap/backgrounds/)
';
- const bgSelect = document.createElement('select');
- bgSelect.className = 's-update inputGrey2';
- bgSelect.innerHTML = '';
- bgRow.appendChild(bgSelect);
- const bgFolderBtn = document.createElement('div');
- bgFolderBtn.className = 'settingsBtn';
- bgFolderBtn.title = 'Open Backgrounds Folder';
- bgFolderBtn.innerHTML = 'folder';
- bgFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-backgrounds-folder'));
- bgRow.appendChild(bgFolderBtn);
- body.appendChild(bgRow);
-
- ipcRenderer.invoke('list-loading-themes').then((themes: Array<{ id: string; label: string }>) => {
- bgSelect.innerHTML = '';
- for (const t of themes) {
- const opt = document.createElement('option');
- opt.value = t.id;
- opt.textContent = t.label;
- if (t.id === ui.loadingTheme) opt.selected = true;
- bgSelect.appendChild(opt);
- }
- });
-
- bgSelect.addEventListener('change', () => {
- ui.loadingTheme = bgSelect.value;
- saveUI();
- onSettingChanged('refresh');
- });
-}
-
-function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
- 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);
- }
-
- body.appendChild(createToggleRow({
- label: 'Custom Matchmaker',
- desc: 'Use the matchmaker hotkey to find a game matching your criteria',
- checked: mm.enabled, instant: true,
- onChange: (v) => { mm.enabled = v; saveMM(); },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Open Server Browser on Cancel',
- desc: 'Opens the server browser when no game is found and you cancel',
- checked: mm.openServerBrowser, instant: true,
- 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();
- }, undefined, true));
- body.appendChild(createKeybindRow('Matchmaker Accept', 'Key to accept a found game', bag.binds.matchmakerAccept, (b) => {
- bag.binds.matchmakerAccept = b;
- bag.saveBinds();
- }, undefined, true));
- body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => {
- bag.binds.matchmakerCancel = b;
- bag.saveBinds();
- }, undefined, true));
-
- body.appendChild(createNumberRow({
- label: 'Min Players', desc: 'Minimum player count in lobby (0-7)',
- min: 0, max: 7, value: mm.minPlayers, instant: true,
- onChange: (v) => { mm.minPlayers = v; saveMM(); },
- }));
-
- body.appendChild(createNumberRow({
- label: 'Max Players', desc: 'Maximum player count in lobby (0-7)',
- min: 0, max: 7, value: mm.maxPlayers, instant: true,
- onChange: (v) => { mm.maxPlayers = v; saveMM(); },
- }));
-
- body.appendChild(createNumberRow({
- label: 'Min Remaining Time', desc: 'Minimum seconds remaining in match (0-480)',
- min: 0, max: 480, value: mm.minRemainingTime, instant: true,
- onChange: (v) => { mm.minRemainingTime = v; saveMM(); },
- }));
-
- body.appendChild(createCheckboxGrid({
- header: 'Regions (none selected = all)',
- items: MATCHMAKER_REGIONS.map(r => ({ value: r, label: MATCHMAKER_REGION_NAMES[r] || r })),
- selected: mm.regions,
- onChange: () => saveMM(),
- }));
-
- body.appendChild(createCheckboxGrid({
- header: 'Gamemodes (none selected = all)',
- items: MATCHMAKER_GAMEMODE_FILTER.map(gm => ({ value: gm, label: gm })),
- selected: mm.gamemodes,
- onChange: () => saveMM(),
- }));
-
- if (!mm.maps) mm.maps = [];
- body.appendChild(createCheckboxGrid({
- header: 'Maps (none selected = all)',
- items: MATCHMAKER_MAP_FILTER.map(m => ({ value: m, label: MATCHMAKER_MAP_NAMES[m] || m })),
- selected: mm.maps,
- onChange: () => saveMM(),
- }));
-}
-
-function buildDiscordSection(body: HTMLElement, discordConf: any): void {
- const discord = { enabled: false, ...discordConf };
-
- body.appendChild(createToggleRow({
- label: 'Discord Rich Presence',
- desc: 'Show game activity in your Discord profile',
- checked: discord.enabled,
- restart: true,
- onChange: (v) => {
- discord.enabled = v;
- ipcRenderer.invoke('set-config', 'discord', discord);
- },
- }));
-}
-
-// ── Alt Manager helpers ──
-function switchToAccount(account: { username: string; password: string }): void {
- const w = window as any;
- if (typeof w.loginOrRegister !== 'function') return;
-
- function doLogin(): void {
- w.loginOrRegister();
- queueMicrotask(() => {
- const toggleBtn = document.querySelector('.auth-toggle-btn') as HTMLElement;
- if (toggleBtn && toggleBtn.textContent?.includes('username')) toggleBtn.click();
- queueMicrotask(() => {
- const nameInput = document.querySelector('#accName') as HTMLInputElement;
- const passInput = document.querySelector('#accPass') as HTMLInputElement;
- if (!nameInput || !passInput) return;
- nameInput.value = account.username;
- passInput.value = account.password;
- nameInput.dispatchEvent(new Event('input', { bubbles: true }));
- passInput.dispatchEvent(new Event('input', { bubbles: true }));
- const submitBtn = document.querySelector('.io-button') as HTMLElement;
- if (submitBtn) submitBtn.click();
- });
- });
- }
-
- if (typeof w.logoutAcc === 'function') {
- w.logoutAcc();
- setTimeout(doLogin, 500);
- } else {
- doLogin();
- }
-}
-
-function buildAccountsSection(body: HTMLElement, accountsArr: any[]): void {
- const accounts: any[] = accountsArr || [];
-
- const addBtn = document.createElement('div');
- addBtn.className = 'setting settName safety-0 has-button';
- addBtn.innerHTML =
- 'Add Account' +
- '' +
- 'Save a Krunker account for quick switching
';
- body.appendChild(addBtn);
-
- const form = document.createElement('div');
- form.className = 'kpc-acc-form';
- form.style.display = 'none';
- form.innerHTML =
- '' +
- '' +
- '' +
- '' +
- '' +
- '' +
- '
';
- body.appendChild(form);
-
- const labelIn = form.querySelector('.kpc-acc-label') as HTMLInputElement;
- const userIn = form.querySelector('.kpc-acc-user') as HTMLInputElement;
- const passIn = form.querySelector('.kpc-acc-pass') as HTMLInputElement;
-
- // Stop Krunker's global keydown handler from eating keystrokes in our inputs
- form.querySelectorAll('input').forEach(input => {
- input.addEventListener('keydown', (e) => e.stopPropagation());
- });
-
- addBtn.querySelector('button')!.addEventListener('click', () => {
- form.style.display = form.style.display === 'none' ? '' : 'none';
- });
-
- form.querySelector('.kpc-acc-cancel')!.addEventListener('click', () => {
- form.style.display = 'none';
- });
-
- const listEl = document.createElement('div');
- body.appendChild(listEl);
-
- function renderList(): void {
- listEl.innerHTML = '';
- if (accounts.length === 0) {
- listEl.innerHTML = 'No saved accounts
';
- return;
- }
- accounts.forEach((acc, i) => {
- const row = document.createElement('div');
- row.className = 'kpc-acc-item';
- row.innerHTML =
- '' +
- '' + escapeHtml(acc.label) + '' +
- '
' +
- '' +
- '' +
- '' +
- '
';
- row.querySelector('.kpc-acc-switch')!.addEventListener('click', () => {
- ipcRenderer.invoke('alt-get-credentials', i).then((creds: { username: string; password: string } | null) => {
- if (creds) switchToAccount(creds);
- });
- });
- row.querySelector('.kpc-acc-delete')!.addEventListener('click', () => {
- ipcRenderer.invoke('alt-remove', i).then(() => {
- accounts.splice(i, 1);
- renderList();
- });
- });
- listEl.appendChild(row);
- });
- }
- renderList();
-
- form.querySelector('.kpc-acc-save')!.addEventListener('click', () => {
- const label = labelIn.value.trim();
- const user = userIn.value.trim();
- const pass = passIn.value;
- if (!label || !user || !pass) return;
- const newAcc = { label, username: user, password: pass };
- ipcRenderer.invoke('alt-save', newAcc).then(() => {
- accounts.push({ label });
- labelIn.value = '';
- userIn.value = '';
- passIn.value = '';
- form.style.display = 'none';
- renderList();
- });
- });
-}
-
-function buildChatSection(body: HTMLElement, gameConf: any, translatorConf: any, bag: SettingsBag): void {
- const game = { betterChat: true, chatHistorySize: 200, ...gameConf };
-
- function saveGame(): void {
- ipcRenderer.invoke('set-config', 'game', game);
- }
-
- body.appendChild(createToggleRow({
- label: 'Better Chat',
- desc: 'Merge team and all-chat with colored [T]/[M] prefixes',
- checked: game.betterChat, instant: true,
- onChange: (v) => { game.betterChat = v; saveGame(); setBetterChat(v); },
- }));
-
- body.appendChild(createNumberRow({
- label: 'Chat History Size', desc: 'Maximum chat messages to keep (0 to disable history preservation)',
- min: 0, max: 1000, value: game.chatHistorySize, instant: true,
- onChange: (v) => { game.chatHistorySize = v; saveGame(); setChatHistorySize(v); },
- }));
-
- body.appendChild(createKeybindRow('Pause Chat', 'Freeze chat auto-scroll to read history (default F10)', bag.binds.pauseChat, (b) => {
- bag.binds.pauseChat = b;
- bag.saveBinds();
- }, undefined, true));
-
- // Translator settings inline
- const tl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
-
- function saveTL(): void {
- ipcRenderer.invoke('set-config', 'translator', tl);
- }
-
- body.appendChild(createToggleRow({
- label: 'Chat Translator',
- desc: 'Automatically translate non-English chat messages',
- checked: tl.enabled, instant: true,
- onChange: (v) => {
- tl.enabled = v;
- saveTL();
- updateTranslatorConfig({ enabled: v });
- },
- }));
-
- body.appendChild(createSelectRow({
- label: 'Target Language',
- desc: 'Language to translate messages into', instant: true,
- options: [
- { value: 'en', label: 'English' },
- { value: 'es', label: 'Spanish' },
- { value: 'fr', label: 'French' },
- { value: 'de', label: 'German' },
- { value: 'pt', label: 'Portuguese' },
- { value: 'ru', label: 'Russian' },
- { value: 'ja', label: 'Japanese' },
- { value: 'ko', label: 'Korean' },
- { value: 'zh', label: 'Chinese' },
- { value: 'ar', label: 'Arabic' },
- { value: 'hi', label: 'Hindi' },
- { value: 'tr', label: 'Turkish' },
- { value: 'pl', label: 'Polish' },
- { value: 'it', label: 'Italian' },
- { value: 'nl', label: 'Dutch' },
- ],
- value: tl.targetLanguage,
- onChange: (v) => {
- tl.targetLanguage = v;
- saveTL();
- updateTranslatorConfig({ targetLanguage: v });
- },
- }));
-
- body.appendChild(createToggleRow({
- label: 'Show Language Tag',
- desc: 'Show detected language code before translations (e.g. [FR])',
- checked: tl.showLanguageTag, instant: true,
- onChange: (v) => {
- tl.showLanguageTag = v;
- saveTL();
- updateTranslatorConfig({ showLanguageTag: v });
- },
- }));
-}
-
-function buildAdvancedSection(
- body: HTMLElement, advConf: any, perfConf: any, isWindows: boolean,
-): void {
- const advDefaults = {
- removeUselessFeatures: true,
- gpuRasterizing: false,
- helpfulFlags: true,
- increaseLimits: false,
- lowLatency: false,
- experimentalFlags: false,
- angleBackend: 'default',
- verboseLogging: false,
- };
- const adv = { ...advDefaults, ...advConf };
- const perf = { cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
-
- function savePerf(): void {
- ipcRenderer.invoke('set-config', 'performance', perf);
- }
-
- function saveAdv(): void {
- ipcRenderer.invoke('set-config', 'advanced', adv);
- }
-
- const angleOptions: Array<{ value: string; label: string }> = isWindows
- ? [
- { value: 'default', label: 'Default (D3D11)' },
- { value: 'gl', label: 'OpenGL' },
- { value: 'd3d9', label: 'Direct3D 9' },
- { value: 'd3d11', label: 'Direct3D 11' },
- { value: 'd3d11on12', label: 'D3D11on12' },
- { value: 'vulkan', label: 'Vulkan' },
- ]
- : [
- { value: 'default', label: 'Default' },
- { value: 'gl', label: 'OpenGL' },
- { value: 'vulkan', label: 'Vulkan' },
- ];
-
- body.appendChild(createSelectRow({
- label: 'ANGLE Backend',
- desc: 'Graphics API used for WebGL rendering',
- options: angleOptions,
- value: adv.angleBackend, restart: true,
- onChange: (v) => { adv.angleBackend = v; saveAdv(); },
- }));
-
- const advToggles: Array<{ key: string; label: string; desc: string; safety: number }> = [
- { key: 'removeUselessFeatures', label: 'Remove Useless Features', desc: 'Disables crash reporting, metrics, print preview, and other unused Chromium features', safety: 1 },
- { key: 'gpuRasterizing', label: 'GPU Rasterization', desc: 'Force GPU rasterization and out-of-process rasterization', safety: 2 },
- { key: 'helpfulFlags', label: 'Useful Flags', desc: 'Enables WebGL, JS harmony, V8 features, background throttle prevention, and autoplay bypass', safety: 3 },
- { key: 'increaseLimits', label: 'Increase Limits', desc: 'Raises renderer process, WebGL context, and WebRTC CPU limits; ignores GPU blocklist', safety: 4 },
- { key: 'lowLatency', label: 'Low Latency Flags', desc: 'Enables high-resolution timer, QUIC protocol, and high-performance GPU', safety: 4 },
- { key: 'experimentalFlags', label: 'Experimental Flags', desc: 'Enables accelerated video decode, native GPU memory buffers, high DPI support, and disables pings/proxy', safety: 4 },
- ];
-
- for (const t of advToggles) {
- body.appendChild(createToggleRow({
- label: t.label, desc: t.desc,
- checked: !!adv[t.key], restart: true,
- safety: t.safety,
- onChange: (v) => { adv[t.key] = v; saveAdv(); },
- }));
- }
-
- body.appendChild(createNumberRow({
- label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
- min: 1, max: 3, value: perf.cpuThrottleGame, instant: true, safety: 2,
- onChange: (v) => { perf.cpuThrottleGame = v; savePerf(); },
- }));
-
- body.appendChild(createNumberRow({
- label: 'CPU Throttle (Menu)', desc: 'CPU throttle rate on menu screens (1 = no throttle, 3 = heavy throttle)',
- min: 1, max: 3, value: perf.cpuThrottleMenu, instant: true, safety: 1,
- onChange: (v) => { perf.cpuThrottleMenu = v; savePerf(); },
- }));
-
- if (isWindows) {
- body.appendChild(createSelectRow({
- label: 'Process Priority',
- desc: 'OS-level process priority for the client (Windows only)',
- options: [
- { value: 'Normal', label: 'Normal' },
- { value: 'Above Normal', label: 'Above Normal' },
- { value: 'High', label: 'High' },
- { value: 'Below Normal', label: 'Below Normal' },
- { value: 'Low', label: 'Low' },
- ],
- value: perf.processPriority, restart: true, safety: 2,
- onChange: (v) => { perf.processPriority = v; savePerf(); },
- }));
- }
-
- body.appendChild(createToggleRow({
- label: 'Verbose Logging',
- desc: 'Forward all preload console output to the Electron log file',
- checked: adv.verboseLogging, instant: true,
- onChange: (v) => {
- adv.verboseLogging = v; saveAdv();
- _verboseLogging = v;
- },
- }));
-}
-
-// ── Search filter + "no settings" cleanup ──
-function applySearchFilter(container: HTMLElement, holder: HTMLElement, searchQuery: string): void {
- const query = searchQuery.toLowerCase();
- const sections = Array.from(container.children).filter(el => el.querySelector('.setHed'));
- sections.forEach(sectionEl => {
- const sectionTitle = sectionEl.querySelector('.setHed')?.textContent?.toLowerCase() || '';
- const body = sectionEl.querySelector('.setBodH');
- if (!body) { (sectionEl as HTMLElement).style.display = 'none'; return; }
-
- if (sectionTitle.includes(query)) {
- body.classList.remove('setting-category-collapsed');
- return;
- }
-
- let visibleCount = 0;
- Array.from(body.children).forEach(child => {
- const el = child as HTMLElement;
- const text = el.textContent?.toLowerCase() || '';
- if (text.includes(query)) {
- el.style.display = '';
- visibleCount++;
- } else {
- el.style.display = 'none';
- }
- });
- if (visibleCount === 0) {
- (sectionEl as HTMLElement).style.display = 'none';
- } else {
- body.classList.remove('setting-category-collapsed');
- }
- });
-
- const hasVisible = sections.find(el => (el as HTMLElement).style.display !== 'none');
- if (hasVisible) {
- Array.from(holder.children).forEach(child => {
- if ((child as HTMLElement).textContent?.toLowerCase().includes('no settings')) {
- (child as HTMLElement).remove();
- }
- });
- }
-}
-
-function renderSettings(searchQuery?: string): void {
- const holder = document.getElementById('settHolder');
- if (!holder) return;
-
- refreshLevel = RefreshLevel.none;
- if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
-
- if (searchQuery) {
- const existing = holder.querySelector('.kpc-settings');
- if (existing) existing.remove();
- } else {
- while (holder.firstChild) holder.removeChild(holder.firstChild);
- }
-
- const container = document.createElement('div');
- container.className = 'kpc-settings';
-
- // ── Action button grid ──
- const actionGrid = document.createElement('div');
- actionGrid.className = 'kpc-action-grid';
-
- const actionButtons: Array<{ label: string; color: string; full?: boolean; action: () => void }> = [
- { label: 'Open Resource Swapper', color: 'kpc-ab-pink', action: () => ipcRenderer.invoke('open-swap-folder') },
- { label: 'Reset Resource Swapper', color: 'kpc-ab-pink', action: () => {
- if (confirm('Reset resource swapper? This will delete all files in the swapper folder.')) {
- ipcRenderer.invoke('reset-swapper');
- }
- }},
- { label: 'Open Electron Logs', color: 'kpc-ab-red', action: () => ipcRenderer.invoke('open-electron-log') },
- { label: 'Restart Client', color: 'kpc-ab-orange', full: true, action: () => ipcRenderer.invoke('restart-client') },
- { label: 'Reset Options', color: 'kpc-ab-red', action: () => {
- if (confirm('Reset all settings to defaults? The client will restart.')) {
- ipcRenderer.invoke('reset-options');
- }
- }},
- { label: 'Delete All Data', color: 'kpc-ab-red', action: () => {
- if (confirm('Delete all data (config, logs)? Scripts are preserved. The client will restart.')) {
- ipcRenderer.invoke('delete-all-data');
- }
- }},
- ];
-
- for (const ab of actionButtons) {
- const btn = document.createElement('button');
- btn.className = 'kpc-action-btn ' + ab.color + (ab.full ? ' full' : '');
- btn.textContent = ab.label;
- btn.addEventListener('click', ab.action);
- actionGrid.appendChild(btn);
- }
- container.appendChild(actionGrid);
-
- // ── Create section shells ──
- const genSec = createSection('General');
- container.appendChild(genSec.section);
- const swapSec = createSection('Swapper');
- container.appendChild(swapSec.section);
- const mmSec = createSection('Matchmaker');
- container.appendChild(mmSec.section);
- const chatSec = createSection('Chat');
- container.appendChild(chatSec.section);
- const discordSec = createSection('Discord');
- container.appendChild(discordSec.section);
- const accSec = createSection('Accounts', true);
- container.appendChild(accSec.section);
- const advSec = createSection('Advanced');
- container.appendChild(advSec.section);
- const usSec = createSection('Userscripts');
- container.appendChild(usSec.section);
-
- // Load all configs in a single IPC call + platform info
- Promise.all([
- ipcRenderer.invoke('get-all-config', ['swapper', 'matchmaker', 'keybinds', 'advanced', 'game', 'ui', 'discord', 'translator', 'accounts', 'performance']),
- ipcRenderer.invoke('get-platform'),
- ]).then(([allConf, platformInfo]: [any, any]) => {
- const swapperConf = allConf.swapper;
- const mmConf = allConf.matchmaker;
- const keybindsConf = allConf.keybinds;
- const advConf = allConf.advanced;
- const gameConf = allConf.game;
- const uiConfRaw = allConf.ui;
- const discordConf = allConf.discord;
- const translatorConf = allConf.translator;
- const defaultBinds = {
- matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
- matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
- matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
- pauseChat: { key: 'F10', ctrl: false, shift: false, alt: false },
- fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
- };
- const binds = { ...defaultBinds, ...keybindsConf };
- const isWindows = platformInfo && platformInfo.isWindows;
-
- const bag: SettingsBag = {
- binds,
- saveBinds: () => ipcRenderer.invoke('set-config', 'keybinds', binds),
- isWindows,
- };
-
- // Populate each section
- buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag);
- buildSwapperSection(swapSec.body, swapperConf, uiConfRaw);
- buildMatchmakerSection(mmSec.body, mmConf, bag);
- buildChatSection(chatSec.body, gameConf, translatorConf, bag);
- buildDiscordSection(discordSec.body, discordConf);
- buildAccountsSection(accSec.body, allConf.accounts);
- buildAdvancedSection(advSec.body, advConf, allConf.performance, isWindows);
- renderUserscriptsSection(usSec.body);
-
- if (searchQuery) applySearchFilter(container, holder, searchQuery);
-
- holder.appendChild(container);
- }).catch((err: any) => {
- console.error('[KCC] Settings render error:', err);
- });
-}
-
-// ── Userscripts settings section ──
-function renderUserscriptsSection(body: HTMLElement): void {
- ipcRenderer.invoke('get-config', 'userscripts').then((usConf: any) => {
- const us = usConf || { enabled: true, path: '' };
-
- body.appendChild(createToggleRow({
- label: 'Userscripts',
- desc: 'Load custom scripts from the scripts folder',
- checked: us.enabled, restart: true,
- onChange: (v) => { us.enabled = v; ipcRenderer.invoke('set-config', 'userscripts', us); },
- }));
-
- const usFolderRow = document.createElement('div');
- usFolderRow.className = 'setting settName safety-0 has-button';
- usFolderRow.innerHTML =
- 'Scripts Folder' +
- 'Place .js userscript files here
';
- const usFolderBtn = document.createElement('div');
- usFolderBtn.className = 'settingsBtn';
- usFolderBtn.title = 'Open Folder';
- usFolderBtn.innerHTML = 'folder Scripts';
- usFolderBtn.addEventListener('click', () => ipcRenderer.invoke('userscripts-open-folder'));
- usFolderRow.appendChild(usFolderBtn);
- body.appendChild(usFolderRow);
-
- const scriptInstances = getInstances();
- if (scriptInstances.length === 0) {
- const emptyRow = document.createElement('div');
- emptyRow.className = 'setting settName safety-0';
- emptyRow.innerHTML =
- 'No userscripts found. Place .js files in the scripts folder and reload.
';
- body.appendChild(emptyRow);
- return;
- }
-
- for (const inst of scriptInstances) {
- const scriptRow = document.createElement('div');
- scriptRow.className = 'setting settName safety-0 bool';
-
- const displayName = escapeHtml(inst.meta.name || inst.filename);
- const metaParts: string[] = [];
- if (inst.meta.author) metaParts.push('by ' + escapeHtml(inst.meta.author));
- if (inst.meta.version) metaParts.push('v' + escapeHtml(inst.meta.version));
- const metaLine = metaParts.length > 0 ? '' + metaParts.join(' · ') + '' : '';
- const descText = escapeHtml(inst.meta.desc || '');
-
- scriptRow.innerHTML =
- '' + displayName + '' +
- '' +
- '' + descText + (metaLine ? '
' + metaLine : '') + '
';
- body.appendChild(scriptRow);
-
- const cb = scriptRow.querySelector('input[type="checkbox"]') as HTMLInputElement;
- const settingsContainer = document.createElement('div');
- settingsContainer.className = 'kpc-us-settings';
- body.appendChild(settingsContainer);
-
- if (inst.enabled && inst.settings) {
- renderScriptSettings(inst, settingsContainer);
- }
-
- cb.addEventListener('change', () => {
- const { needsReload } = setScriptEnabled(inst.filename, cb.checked, _console);
- settingsContainer.innerHTML = '';
- if (cb.checked && inst.settings) {
- renderScriptSettings(inst, settingsContainer);
- }
- if (needsReload) {
- onSettingChanged('refresh');
- }
- });
- }
- });
-}
-
-function renderScriptSettings(inst: UserscriptInstance, container: HTMLElement): void {
- if (!inst.settings) return;
-
- for (const [, setting] of Object.entries(inst.settings)) {
- const typeClass = setting.type === 'bool' ? 'bool' : setting.type === 'sel' ? 'sel' : setting.type === 'num' ? 'num' : setting.type === 'keybind' ? 'keybind' : '';
- const row = document.createElement('div');
- row.className = 'setting settName safety-0' + (typeClass ? ' ' + typeClass : '');
- row.innerHTML =
- '' + escapeHtml(setting.title) + '' +
- (setting.desc ? '' + escapeHtml(setting.desc) + '
' : '');
-
- switch (setting.type) {
- case 'bool': {
- const label = document.createElement('label');
- label.className = 'switch';
- label.innerHTML =
- '' +
- '';
- row.appendChild(label);
- const input = label.querySelector('input') as HTMLInputElement;
- input.addEventListener('change', () => {
- setting.value = input.checked;
- if (typeof setting.changed === 'function') setting.changed(setting.value);
- saveScriptSetting(inst);
- });
- break;
- }
- case 'num': {
- const input = document.createElement('input');
- input.type = 'number';
- input.className = 'rb-input s-update sliderVal';
- input.value = String(setting.value);
- if (setting.min !== undefined) input.min = String(setting.min);
- if (setting.max !== undefined) input.max = String(setting.max);
- if (setting.step !== undefined) input.step = String(setting.step);
- row.appendChild(input);
- input.addEventListener('change', () => {
- setting.value = parseFloat(input.value) || 0;
- if (typeof setting.changed === 'function') setting.changed(setting.value);
- saveScriptSetting(inst);
- });
- break;
- }
- case 'sel': {
- const select = document.createElement('select');
- select.className = 's-update inputGrey2';
- if (setting.opts) {
- for (const opt of setting.opts) {
- const option = document.createElement('option');
- option.value = String(opt);
- option.textContent = String(opt);
- if (String(opt) === String(setting.value)) option.selected = true;
- select.appendChild(option);
- }
- }
- row.appendChild(select);
- select.addEventListener('change', () => {
- setting.value = select.value;
- if (typeof setting.changed === 'function') setting.changed(setting.value);
- saveScriptSetting(inst);
- });
- break;
- }
- case 'color': {
- const input = document.createElement('input');
- input.type = 'color';
- input.className = 'kpc-color-input';
- input.value = String(setting.value) || '#ffffff';
- row.appendChild(input);
- input.addEventListener('input', () => {
- setting.value = input.value;
- if (typeof setting.changed === 'function') setting.changed(setting.value);
- saveScriptSetting(inst);
- });
- break;
- }
- case 'keybind': {
- const bind = setting.value as Keybind;
- const keyEl = document.createElement('span');
- keyEl.className = 'keyIcon kpc-keyIcon';
- keyEl.textContent = keybindDisplayString(bind);
- keyEl.addEventListener('click', () => {
- openKeybindDialog(setting.title).then((newBind) => {
- setting.value = newBind;
- keyEl.textContent = keybindDisplayString(newBind);
- if (typeof setting.changed === 'function') setting.changed(setting.value);
- saveScriptSetting(inst);
- });
- });
- row.appendChild(keyEl);
- break;
- }
- }
-
- container.appendChild(row);
- }
-}
-
-function saveScriptSetting(inst: UserscriptInstance): void {
- if (!inst.settings) return;
- const prefs: Record = {};
- for (const [k, s] of Object.entries(inst.settings)) {
- prefs[k] = s.value;
- }
- ipcRenderer.invoke('userscripts-save-prefs', inst.filename, prefs);
-}
-
-// ── Hide menu popups (polling-based, safe per MutationObserver constraint) ──
-let _hidePopupsInterval: ReturnType | null = null;
-const HIDE_POPUPS_CSS =
- '#leftTabsHolder > .youNewDiv:not(#battlepassAd), .webpush-container, ' +
- '#homeStoreAd, #streamContainerNew, #bundlePop, #genericPop.claimPop, ' +
- '#newsHolder, #streamContainer { display: none !important; }';
-const HIDE_POPUPS_ELS = ['homeStoreAd', 'streamContainerNew'];
-
-function startHidePopups(): void {
- if (_hidePopupsInterval) return;
- if (!document.getElementById('kpc-hideMenuPopups')) {
- const style = document.createElement('style');
- style.id = 'kpc-hideMenuPopups';
- style.textContent = HIDE_POPUPS_CSS;
- document.head.appendChild(style);
- }
- const w = window as any;
- _hidePopupsInterval = setInterval(() => {
- for (const id of HIDE_POPUPS_ELS) {
- const el = document.getElementById(id);
- if (el && el.style.display !== 'none') el.style.display = 'none';
- }
- const bundlePop = document.getElementById('bundlePop');
- if (bundlePop && bundlePop.children.length > 0 && bundlePop.style.display !== 'none') {
- if (typeof w.clearPops === 'function') w.clearPops();
- }
- const genericPop = document.getElementById('genericPop');
- if (genericPop && genericPop.classList.contains('claimPop') && genericPop.style.display !== 'none') {
- if (typeof w.clearPops === 'function') w.clearPops();
- }
- }, 1000);
-}
-
-function stopHidePopups(): void {
- if (_hidePopupsInterval) { clearInterval(_hidePopupsInterval); _hidePopupsInterval = null; }
- const style = document.getElementById('kpc-hideMenuPopups');
- if (style) style.remove();
- for (const id of HIDE_POPUPS_ELS) {
- const el = document.getElementById(id);
- if (el) el.style.display = '';
- }
-}
-
-// ── Matchmaker IPC listener ──
-ipcRenderer.on('matchmaker-find', (_e, mmConfig: MatchmakerConfig) => {
- fetchGame(mmConfig, _console).catch((err) => _console.error('[KCC] Matchmaker error:', err));
-});
-
-// ── Chat pause ──
-let chatPaused = false;
-let chatSavedScrollTop = 0;
-
-function onChatWheel(e: WheelEvent): void {
- const chatList = document.getElementById('chatList');
- if (!chatList) return;
- chatSavedScrollTop = Math.max(0, Math.min(
- chatSavedScrollTop + e.deltaY,
- chatList.scrollHeight - chatList.clientHeight,
- ));
- chatList.scrollTop = chatSavedScrollTop;
-}
-
-ipcRenderer.on('toggle-chat-pause', () => {
- const chatList = document.getElementById('chatList');
- if (!chatList) return;
-
- chatPaused = !chatPaused;
-
- if (chatPaused) {
- chatSavedScrollTop = chatList.scrollTop;
- chatList.classList.add('kpc-chat-paused');
- chatList.style.overflow = 'hidden';
- chatList.addEventListener('wheel', onChatWheel, { passive: true });
- } else {
- chatList.classList.remove('kpc-chat-paused');
- chatList.style.overflow = '';
- chatList.removeEventListener('wheel', onChatWheel);
- chatList.scrollTop = chatList.scrollHeight;
- }
-});
-
-// ── Wait for main process to signal page load, then poll for settings window ──
-ipcRenderer.on('main_did-finish-load', () => {
- _console.log('[KCC] did-finish-load received, waiting to hook settings...');
-
- const isGamePage = window.location.pathname === '/' || window.location.pathname === '';
-
- // ── Batch all config reads into a single IPC call ──
- (window as any).closeClient = () => window.close();
- Promise.all([
- ipcRenderer.invoke('get-all-config', ['ui', 'userscripts', 'game', 'translator', 'keybinds', 'discord', 'advanced', 'performance']),
- ipcRenderer.invoke('get-platform'),
- ipcRenderer.invoke('get-version'),
- ]).then(([allConf, _platformInfo, currentVersion]: [any, any, string]) => {
- const uiConf = allConf.ui;
- const usConf = allConf.userscripts;
- const gameConf = allConf.game;
- const translatorConf = allConf.translator;
- const discordConf = allConf.discord;
- const advConf = allConf.advanced;
-
- // ── Verbose logging toggle ──
- _verboseLogging = advConf?.verboseLogging ?? false;
-
- // ── Exit button + UI toggles ──
- const showExit = uiConf ? (uiConf.showExitButton !== false) : true;
- const showExitBtn = () => {
- const btn = document.getElementById('clientExit');
- if (btn) {
- btn.style.display = showExit ? 'flex' : 'none';
- return true;
- }
- return false;
- };
- if (!showExitBtn()) {
- let exitAttempts = 0;
- const exitPoll = setInterval(() => {
- if (showExitBtn() || ++exitAttempts > 30) clearInterval(exitPoll);
- }, 500);
- }
-
- if (uiConf?.deathscreenAnimation) setDeathAnimBlock(true);
- if (uiConf?.hideMenuPopups) startHidePopups();
- if (uiConf?.menuTimer ?? true) setMenuTimer(true);
-
- // ── Double ping display ──
- if (isGamePage && (uiConf?.doublePing ?? true)) {
- initDoublePing();
- }
-
- // ── Show ping in player list ──
- if (isGamePage && (gameConf?.showPing ?? true)) {
- initShowPing();
- }
-
- // ── Raw input (Windows only — unadjustedMovement) ──
- if (isGamePage && process.platform === 'win32' && (gameConf?.rawInput ?? true)) {
- const origLock = HTMLCanvasElement.prototype.requestPointerLock;
- HTMLCanvasElement.prototype.requestPointerLock = function (opts?: any) {
- const promise = origLock.call(this, { ...opts, unadjustedMovement: true }) as any;
- if (promise && typeof promise.catch === 'function') {
- return promise.catch(() => origLock.call(this, opts));
- }
- return promise;
- };
- }
-
- // ── Better chat + Chat history ──
- if (isGamePage) {
- initChat({
- betterChat: gameConf?.betterChat ?? true,
- chatHistorySize: gameConf?.chatHistorySize ?? 200,
- }, _console);
- }
-
- // ── Competitive features ──
- if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) {
- initHPCounter();
- }
- if (isGamePage) {
- initRankProgress();
- }
-
- // ── CPU throttle state notifications ──
- if (isGamePage) {
- let inGame = false;
- setInterval(() => {
- const uiBase = document.getElementById('uiBase');
- const nowInGame = !!uiBase && uiBase.className !== 'onMenu' && uiBase.className !== '';
- if (nowInGame !== inGame) {
- inGame = nowInGame;
- ipcRenderer.send('throttle-state', inGame ? 'game' : 'menu');
- }
- }, 2000);
- }
-
- // ── Changelog popup ──
- if (isGamePage && (uiConf?.showChangelog ?? true)) {
- checkChangelog(currentVersion, uiConf?.lastSeenVersion || '');
- }
-
- // ── Battle Pass Claim All (game page only) ──
- // Poll for .bpBotH element — injects button when BP window is visible
- if (isGamePage) {
- const getClaimable = () => Array.from(document.querySelectorAll('.bpClaimB')).filter(
- (el: any) => el.offsetParent !== null && el.textContent?.trim() === 'Claim'
- );
- setInterval(() => {
- const bar = document.querySelector('.bpBotH') as HTMLElement | null;
- if (!bar || bar.offsetParent === null) return;
- const existing = document.getElementById('claimAllBtn');
- if (existing) {
- // Update state on re-check (rewards may have become claimable)
- const claimable = getClaimable();
- if (claimable.length > 0) {
- existing.textContent = 'Claim All';
- existing.classList.remove('disabled');
- } else {
- existing.textContent = 'Nothing to Claim';
- existing.classList.add('disabled');
- }
- return;
- }
- const claimable = getClaimable();
- const btn = document.createElement('div');
- btn.className = 'bpBtn skip';
- btn.id = 'claimAllBtn';
- btn.style.cssText = 'margin-left: 8px; cursor: pointer; background: #4CAF50;';
- if (claimable.length > 0) {
- btn.textContent = 'Claim All';
- } else {
- btn.textContent = 'Nothing to Claim';
- btn.classList.add('disabled');
- }
- btn.addEventListener('click', async () => {
- if (btn.classList.contains('disabled')) return;
- (window as any).playSelect?.(0.1);
- const items = getClaimable();
- if (items.length === 0) return;
- btn.textContent = 'Claiming...';
- btn.classList.add('disabled');
- for (const item of items) {
- (item as HTMLElement).click();
- await new Promise(r => setTimeout(r, 200));
- }
- const remaining = getClaimable();
- btn.textContent = remaining.length > 0 ? 'Claim All' : 'Nothing to Claim';
- btn.classList.toggle('disabled', remaining.length === 0);
- });
- bar.appendChild(btn);
- }, 500);
- }
-
- // ── Initialize userscripts ──
- const usEnabled = usConf ? usConf.enabled : true;
- if (usEnabled) {
- initUserscripts(_console).catch(err => _console.error('[KCC] Userscript init error:', err));
- }
-
- // ── Join as Spectator — auto-enable spectate on regular game join ──
- if (isGamePage && gameConf?.joinAsSpectator) {
- let attempts = 0;
- const poll = setInterval(() => {
- if (++attempts > 300) { clearInterval(poll); return; }
- const uiBase = document.getElementById('uiBase');
- if (!uiBase || uiBase.className === '') return;
- if (uiBase.className === 'onMenu') {
- const specBtn = document.querySelector('#spectButton input') as HTMLInputElement;
- if (specBtn && !specBtn.checked) {
- (window as any).setSpect(1);
- }
- clearInterval(poll);
- } else {
- clearInterval(poll);
- }
- }, 100);
- }
-
- // ── Initialize chat translator (game page only) ──
- if (isGamePage) {
- const mergedTl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
- initTranslator(_console, mergedTl);
- }
-
- // ── Discord Rich Presence game state polling ──
- if (isGamePage && discordConf?.enabled) {
- let lastDetails = '';
- let lastState = '';
- let gameStartTimestamp = Math.floor(Date.now() / 1000);
-
- function pollDiscordState(): void {
- let details: string;
- let state = '';
- let startTimestamp: number | undefined = undefined;
-
- const w = window as any;
- const spectating = w.spectating;
-
- let gameActivity: any = null;
- if (typeof w.getGameActivity === 'function') {
- try { gameActivity = w.getGameActivity(); } catch { /* game API unavailable */ }
- }
-
- if (spectating) {
- details = 'Spectating';
- if (gameActivity?.map) {
- state = gameActivity.map;
- }
- } else {
- const uiBase = document.getElementById('uiBase');
- if (uiBase && uiBase.className === 'onMenu') {
- details = 'In Menus';
- } else {
- if (gameActivity?.mode && gameActivity?.map) {
- details = gameActivity.mode + ' on ' + gameActivity.map;
- } else {
- const mapInfo = document.getElementById('mapInfo');
- details = mapInfo?.textContent || 'Playing Krunker';
- }
-
- if (gameActivity?.class?.name) {
- state = gameActivity.class.name;
- } else {
- const classElem = document.getElementById('menuClassName');
- if (classElem?.textContent) state = classElem.textContent;
- }
-
- startTimestamp = gameStartTimestamp;
- }
- }
-
- if (details !== lastDetails || state !== lastState) {
- if (startTimestamp && lastDetails !== details) {
- gameStartTimestamp = Math.floor(Date.now() / 1000);
- startTimestamp = gameStartTimestamp;
- }
- lastDetails = details;
- lastState = state;
- ipcRenderer.send('discord-update', {
- details,
- state: state || undefined,
- startTimestamp,
- largeImageKey: 'krunker',
- largeImageText: 'Krunker Civilian Client',
- });
- }
- }
-
- pollDiscordState();
- setInterval(pollDiscordState, 5000);
- document.addEventListener('pointerlockchange', pollDiscordState);
- }
- // ── In-game Accounts quick-switch button ──
- if (isGamePage) {
- ipcRenderer.invoke('alt-list').then(() => {
- const altBtn = document.createElement('div');
- altBtn.id = 'kpcAltBtn';
- altBtn.className = 'menuItem';
- altBtn.setAttribute('onmouseenter', 'playTick()');
- altBtn.innerHTML =
- 'people' +
- '';
-
- function showAltManager(): void {
- const windowHolder = document.getElementById('windowHolder') as HTMLElement;
- const menuWindow = document.getElementById('menuWindow') as HTMLElement;
- const windowHeader = document.getElementById('windowHeader') as HTMLElement;
- if (!windowHolder || !menuWindow || !windowHeader) return;
-
- if (windowHolder.style.display !== 'none' && windowHeader.innerText === 'Alt Manager') {
- windowHolder.style.display = 'none';
- return;
- }
-
- windowHolder.className = 'popupWin';
- windowHolder.style.display = 'block';
- menuWindow.classList.value = 'dark';
- menuWindow.style.cssText = 'width:800px;max-height:calc(100% - 330px);overflow-y:auto;top:50%;transform:translate(-50%,-50%);';
- windowHeader.innerText = 'Alt Manager';
-
- function renderAccountList(): void {
- ipcRenderer.invoke('alt-list').then((accs: any[]) => {
- let html =
- 'Alt Manager
' +
- '
' +
- 'Add Account
' +
- '';
-
- if (!accs || accs.length === 0) {
- html += '
No saved accounts
';
- } else {
- accs.forEach((acc, i) => {
- html +=
- '
' +
- '
' + escapeHtml(acc.label) + '' +
- '
' +
- 'login' +
- '
' +
- '
' +
- '
' +
- 'delete' +
- '
' +
- '
';
- });
- }
- html += '
';
- menuWindow.innerHTML = html;
-
- const addBtn = document.getElementById('kpcAltAddBtn');
- if (addBtn) addBtn.addEventListener('click', showAddForm);
-
- menuWindow.querySelectorAll('.kpc-alt-login').forEach((el) => {
- el.addEventListener('click', () => {
- const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
- if (accs[idx]) {
- windowHolder.style.display = 'none';
- ipcRenderer.invoke('alt-get-credentials', idx).then((creds: { username: string; password: string } | null) => {
- if (creds) switchToAccount(creds);
- });
- }
- });
- });
-
- menuWindow.querySelectorAll('.kpc-alt-del').forEach((el) => {
- el.addEventListener('click', () => {
- const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
- if (confirm('Delete account "' + (accs[idx]?.label || '') + '"?')) {
- ipcRenderer.invoke('alt-remove', idx).then(() => renderAccountList());
- }
- });
- });
- });
- }
-
- function showAddForm(): void {
- menuWindow.innerHTML =
- '' +
- '
Add Account
' +
- '
' +
- '
' +
- '
' +
- '
' +
- '
Add Account
' +
- '
Back
' +
- '
' +
- '
';
-
- // Stop Krunker's global keydown handler from eating keystrokes in our inputs
- menuWindow.querySelectorAll('input.accountInput').forEach((input) => {
- input.addEventListener('keydown', (e) => e.stopPropagation());
- });
-
- document.getElementById('kpcAltBackBtn')!.addEventListener('click', renderAccountList);
- document.getElementById('kpcAltSaveBtn')!.addEventListener('click', () => {
- const label = (document.getElementById('kpcAltLabel') as HTMLInputElement).value.trim();
- const user = (document.getElementById('kpcAltUser') as HTMLInputElement).value.trim();
- const pass = (document.getElementById('kpcAltPass') as HTMLInputElement).value;
- if (!label || !user || !pass) return;
- ipcRenderer.invoke('alt-save', {
- label,
- username: user,
- password: pass,
- }).then(() => renderAccountList());
- });
- }
-
- renderAccountList();
- }
-
- altBtn.addEventListener('click', (e) => {
- e.stopPropagation();
- (window as any).playSelect?.();
- showAltManager();
- });
-
- function injectAltBtn(): boolean {
- if (document.getElementById('kpcAltBtn')) return true;
- const menuContainer = document.getElementById('menuItemContainer');
- if (!menuContainer) return false;
- const exitBtn = document.getElementById('clientExit');
- if (exitBtn) {
- menuContainer.insertBefore(altBtn, exitBtn);
- } else {
- menuContainer.appendChild(altBtn);
- }
- return true;
- }
-
- if (!injectAltBtn()) {
- let attempts = 0;
- const poll = setInterval(() => {
- if (injectAltBtn() || ++attempts > 60) clearInterval(poll);
- }, 500);
- }
- });
- }
-
- }).catch(() => {});
-
- const pollInterval = setInterval(() => {
- const w = window as any;
- if (
- Object.hasOwn(w, 'showWindow')
- && typeof w.showWindow === 'function'
- && Object.hasOwn(w, 'windows')
- && Array.isArray(w.windows)
- && w.windows.length >= 0
- && typeof w.windows[0] !== 'undefined'
- && typeof w.windows[0].changeTab === 'function'
- ) {
- clearInterval(pollInterval);
- _console.log('[KCC] Settings window found, hooking...');
- hookSettings();
- }
- }, 500);
-});
-
-// ── Lightweight tab page init (skips game-only features) ──
-ipcRenderer.on('main_did-finish-load-tab', () => {
- _console.log('[KCC] Tab page loaded');
- (window as any).closeClient = () => window.close();
-});
+import { ipcRenderer } from 'electron';
+import { fetchGame, MATCHMAKER_GAMEMODE_FILTER, MATCHMAKER_REGIONS, MATCHMAKER_REGION_NAMES, MATCHMAKER_MAP_FILTER, MATCHMAKER_MAP_NAMES } from './matchmaker';
+import type { MatchmakerConfig } from './matchmaker';
+import { initUserscripts, getInstances, setScriptEnabled } from './userscripts';
+import type { UserscriptInstance } from './userscripts';
+import { initTranslator, updateTranslatorConfig } from './translator';
+import { setDeathAnimBlock, setMenuTimer, escapeHtml } from './utils';
+import { initChat, setBetterChat, setChatHistorySize } from './chat';
+import { initHPCounter, destroyHPCounter, initRankProgress } from './competitive';
+import { checkChangelog } from './changelog';
+import type { Keybind } from '../main/config';
+
+
+// ── Save console methods before Krunker overwrites them ──
+// Wrapped to forward errors/warnings always, and logs when verbose is enabled
+let _verboseLogging = false;
+
+const _console = {
+ log: (...args: unknown[]) => {
+ console.log(...args);
+ if (_verboseLogging) ipcRenderer.send('verbose-log', 'log', ...args);
+ },
+ warn: (...args: unknown[]) => {
+ console.warn(...args);
+ ipcRenderer.send('verbose-log', 'warn', ...args);
+ },
+ error: (...args: unknown[]) => {
+ console.error(...args);
+ ipcRenderer.send('verbose-log', 'error', ...args);
+ },
+};
+
+_console.log('[KCC] Preload script loaded');
+
+// ── Krunker-native settings styling constants (from Crankshaft) ──
+const SAFETY_SVG = '';
+const REFRESH_SVG = '';
+const SAFETY_DESCS = [
+ 'This setting is safe/standard',
+ 'Proceed with caution',
+ 'This setting is not recommended',
+ 'This setting is experimental',
+ 'This setting is experimental and unstable. Use at your own risk.',
+];
+
+const enum RefreshLevel { none, refresh, restart }
+let refreshLevel: number = RefreshLevel.none;
+let refreshPopupEl: HTMLElement | null = null;
+
+function safetyIcon(safety: string): string {
+ return '' + SAFETY_SVG + '';
+}
+
+function refreshIcon(mode: 'instant' | 'refresh-icon'): string {
+ return '' + REFRESH_SVG + '';
+}
+
+function restartIcon(): string {
+ return '' + SAFETY_SVG + '';
+}
+
+function settingIcon(safety: number, instant?: boolean, refreshOnly?: boolean, restart?: boolean): string {
+ if (safety > 0) return safetyIcon(SAFETY_DESCS[safety]);
+ if (instant) return refreshIcon('instant');
+ if (refreshOnly) return refreshIcon('refresh-icon');
+ if (restart) return restartIcon();
+ return '';
+}
+
+function onSettingChanged(level: 'refresh' | 'restart'): void {
+ const newLevel = level === 'restart' ? RefreshLevel.restart : RefreshLevel.refresh;
+ if (newLevel > refreshLevel) refreshLevel = newLevel;
+ updateRefreshNotification();
+}
+
+function updateRefreshNotification(): void {
+ if (refreshLevel === RefreshLevel.none) {
+ if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
+ return;
+ }
+ if (refreshPopupEl) { try { refreshPopupEl.remove(); } catch { /* noop */ } }
+ refreshPopupEl = document.createElement('div');
+ refreshPopupEl.className = 'kpc-holder-update refresh-popup';
+ if (refreshLevel === RefreshLevel.restart) {
+ refreshPopupEl.innerHTML = 'Restart client fully to see changes';
+ } else {
+ refreshPopupEl.innerHTML = '' + refreshIcon('refresh-icon') + 'Reload page with F5 or CTRL + R to see changes';
+ }
+ document.body.appendChild(refreshPopupEl);
+}
+
+// ── Tell Krunker this is a client (enables "Client" settings tab) ──
+(window as any).OffCliV = true;
+
+// ── IPC bridge exposed as window.kpc ──
+(window as any).kpc = {
+ platform: {
+ getInfo: () => ipcRenderer.invoke('get-platform'),
+ },
+ config: {
+ get: (key: string) => ipcRenderer.invoke('get-config', key),
+ getAll: (keys: string[]) => ipcRenderer.invoke('get-all-config', keys),
+ set: (key: string, value: unknown) => ipcRenderer.invoke('set-config', key, value),
+ },
+ window: {
+ minimize: () => ipcRenderer.invoke('window-minimize'),
+ maximize: () => ipcRenderer.invoke('window-maximize'),
+ close: () => ipcRenderer.invoke('window-close'),
+ isMaximized: () => ipcRenderer.invoke('window-is-maximized'),
+ },
+ dev: {
+ toggleDevTools: () => ipcRenderer.invoke('toggle-devtools'),
+ },
+ swapper: {
+ openFolder: () => ipcRenderer.invoke('open-swap-folder'),
+ getPath: () => ipcRenderer.invoke('get-swap-dir'),
+ },
+ userscripts: {
+ openFolder: () => ipcRenderer.invoke('userscripts-open-folder'),
+ getPath: () => ipcRenderer.invoke('userscripts-get-dir'),
+ },
+};
+
+// ── Client settings tab in Krunker's settings ──
+
+// ── Keybind helpers ──
+function keybindDisplayString(bind: Keybind): string {
+ return (bind.shift ? 'Shift+' : '') + (bind.ctrl ? 'Ctrl+' : '') + (bind.alt ? 'Alt+' : '') + bind.key.toUpperCase();
+}
+
+// ── Keybind capture dialog (Crankshaft-style) ──
+let capturingKeybind: { resolve: (bind: Keybind) => void } | null = null;
+
+const kbOverlay = document.createElement('div');
+kbOverlay.className = 'kpc-keybind-overlay';
+const kbDialog = document.createElement('div');
+kbDialog.className = 'kpc-keybind-dialog';
+const kbTitle = document.createElement('div');
+kbTitle.className = 'kpc-keybind-dialog-title';
+const kbSub = document.createElement('div');
+kbSub.className = 'kpc-keybind-dialog-sub';
+kbSub.innerHTML = 'Press any key. Press Shift+Escape to cancel.';
+const kbModifiers = document.createElement('div');
+kbModifiers.className = 'kpc-keybind-dialog-modifiers';
+const kbShift = document.createElement('div');
+kbShift.className = 'kpc-keybind-modifier';
+kbShift.textContent = 'Shift';
+const kbCtrl = document.createElement('div');
+kbCtrl.className = 'kpc-keybind-modifier';
+kbCtrl.textContent = 'Control';
+const kbAlt = document.createElement('div');
+kbAlt.className = 'kpc-keybind-modifier';
+kbAlt.textContent = 'Alt';
+const kbCancel = document.createElement('div');
+kbCancel.className = 'kpc-keybind-dialog-cancel';
+kbCancel.textContent = 'Cancel';
+kbCancel.addEventListener('click', dismissKeybindDialog);
+
+kbModifiers.appendChild(kbShift);
+kbModifiers.appendChild(kbCtrl);
+kbModifiers.appendChild(kbAlt);
+kbDialog.appendChild(kbCancel);
+kbDialog.appendChild(kbTitle);
+kbDialog.appendChild(kbSub);
+kbDialog.appendChild(kbModifiers);
+kbOverlay.appendChild(kbDialog);
+
+function dismissKeybindDialog(): void {
+ kbShift.classList.remove('active');
+ kbCtrl.classList.remove('active');
+ kbAlt.classList.remove('active');
+ document.removeEventListener('keydown', kbKeydownHandler, true);
+ document.removeEventListener('keyup', kbKeyupHandler, true);
+ if (kbOverlay.parentNode) kbOverlay.remove();
+ capturingKeybind = null;
+ ipcRenderer.send('keybind-capture', false);
+}
+
+function kbKeydownHandler(event: KeyboardEvent): void {
+ event.stopImmediatePropagation();
+ event.preventDefault();
+ if (event.key === 'Control') kbCtrl.classList.add('active');
+ else if (event.key === 'Shift') kbShift.classList.add('active');
+ else if (event.key === 'Alt') kbAlt.classList.add('active');
+}
+
+function kbKeyupHandler(event: KeyboardEvent): void {
+ event.stopImmediatePropagation();
+ event.preventDefault();
+ if (!capturingKeybind) return;
+
+ if (event.key === 'Escape' && event.shiftKey) {
+ dismissKeybindDialog();
+ return;
+ }
+
+ // Modifier-only releases just clear indicators
+ if (event.key === 'Shift' || event.key === 'Control' || event.key === 'Alt') {
+ const bind: Keybind = { key: event.key, ctrl: false, shift: false, alt: false };
+ capturingKeybind.resolve(bind);
+ dismissKeybindDialog();
+ return;
+ }
+
+ const bind: Keybind = {
+ key: event.key,
+ ctrl: event.ctrlKey,
+ shift: event.shiftKey,
+ alt: event.altKey,
+ };
+ capturingKeybind.resolve(bind);
+ dismissKeybindDialog();
+}
+
+function openKeybindDialog(title: string): Promise {
+ return new Promise((resolve) => {
+ capturingKeybind = { resolve };
+ kbTitle.textContent = 'Edit Keybind: ' + title;
+ kbShift.classList.remove('active');
+ kbCtrl.classList.remove('active');
+ kbAlt.classList.remove('active');
+ ipcRenderer.send('keybind-capture', true);
+ document.addEventListener('keydown', kbKeydownHandler, true);
+ document.addEventListener('keyup', kbKeyupHandler, true);
+ document.body.appendChild(kbOverlay);
+ });
+}
+
+function createKeybindRow(label: string, desc: string, currentBind: Keybind, onBind: (bind: Keybind) => void, safety?: number, instant?: boolean): HTMLElement {
+ const s = safety || 0;
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-' + s + ' keybind';
+ row.innerHTML =
+ settingIcon(s, instant) +
+ '' + escapeHtml(label) + '' +
+ '' + escapeHtml(keybindDisplayString(currentBind)) + '' +
+ '' + escapeHtml(desc) + '
';
+ const keyEl = row.querySelector('.kpc-keyIcon') as HTMLElement;
+ keyEl.addEventListener('click', () => {
+ openKeybindDialog(label).then((newBind) => {
+ keyEl.textContent = keybindDisplayString(newBind);
+ onBind(newBind);
+ });
+ });
+ return row;
+}
+
+function createToggleRow(opts: {
+ label: string;
+ desc: string;
+ checked: boolean;
+ onChange: (checked: boolean) => void;
+ restart?: boolean;
+ disabled?: boolean;
+ safety?: number;
+ instant?: boolean;
+ refreshOnly?: boolean;
+}): HTMLElement {
+ const s = opts.safety || 0;
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-' + s + ' bool';
+ row.innerHTML =
+ settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
+ '' + escapeHtml(opts.label) + '' +
+ '' +
+ '' + escapeHtml(opts.desc) + '
';
+ if (!opts.disabled) {
+ const cb = row.querySelector('input[type="checkbox"]') as HTMLInputElement;
+ cb.addEventListener('change', () => {
+ opts.onChange(cb.checked);
+ if (opts.restart) onSettingChanged('restart');
+ else if (opts.refreshOnly) onSettingChanged('refresh');
+ });
+ }
+ return row;
+}
+
+function createSelectRow(opts: {
+ label: string;
+ desc: string;
+ options: Array<{ value: string; label: string }>;
+ value: string;
+ onChange: (value: string) => void;
+ restart?: boolean;
+ safety?: number;
+ instant?: boolean;
+ refreshOnly?: boolean;
+}): HTMLElement {
+ const s = opts.safety || 0;
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-' + s + ' sel';
+ row.innerHTML =
+ settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
+ '' + escapeHtml(opts.label) + '' +
+ '' + escapeHtml(opts.desc) + '
';
+ const select = document.createElement('select');
+ select.className = 's-update inputGrey2';
+ for (const o of opts.options) {
+ const option = document.createElement('option');
+ option.value = o.value;
+ option.textContent = o.label;
+ if (o.value === opts.value) option.selected = true;
+ select.appendChild(option);
+ }
+ select.addEventListener('change', () => {
+ opts.onChange(select.value);
+ if (opts.restart) onSettingChanged('restart');
+ else if (opts.refreshOnly) onSettingChanged('refresh');
+ });
+ row.appendChild(select);
+ return row;
+}
+
+function createNumberRow(opts: {
+ label: string;
+ desc: string;
+ min: number;
+ max: number;
+ value: number;
+ onChange: (value: number) => void;
+ safety?: number;
+ restart?: boolean;
+ instant?: boolean;
+ refreshOnly?: boolean;
+}): HTMLElement {
+ const s = opts.safety || 0;
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-' + s + ' num';
+ row.innerHTML =
+ settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
+ '' + escapeHtml(opts.label) + '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' + escapeHtml(opts.desc) + '
';
+ const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement;
+ const numInput = row.querySelector('input[type="number"]') as HTMLInputElement;
+ rangeInput.addEventListener('input', () => {
+ numInput.value = rangeInput.value;
+ });
+ rangeInput.addEventListener('change', () => {
+ const v = Math.max(opts.min, Math.min(opts.max, parseInt(rangeInput.value) || 0));
+ rangeInput.value = String(v);
+ numInput.value = String(v);
+ opts.onChange(v);
+ if (opts.restart) onSettingChanged('restart');
+ else if (opts.refreshOnly) onSettingChanged('refresh');
+ });
+ numInput.addEventListener('change', () => {
+ const v = Math.max(opts.min, Math.min(opts.max, parseInt(numInput.value) || 0));
+ numInput.value = String(v);
+ rangeInput.value = String(v);
+ opts.onChange(v);
+ if (opts.restart) onSettingChanged('restart');
+ else if (opts.refreshOnly) onSettingChanged('refresh');
+ });
+ return row;
+}
+
+function createCheckboxGrid(opts: {
+ header: string;
+ items: Array<{ value: string; label: string }>;
+ selected: string[];
+ onChange: (selected: string[]) => void;
+}): HTMLElement {
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-0 multisel';
+ row.innerHTML = '' + escapeHtml(opts.header) + '';
+ const grid = document.createElement('div');
+ grid.className = 'kpc-multisel-parent';
+ for (const item of opts.items) {
+ const label = document.createElement('label');
+ label.className = 'hostOpt';
+ label.innerHTML =
+ '' + escapeHtml(item.label) + '' +
+ '' +
+ '';
+ const cb = label.querySelector('input') as HTMLInputElement;
+ cb.addEventListener('change', () => {
+ if (cb.checked) {
+ if (!opts.selected.includes(item.value)) opts.selected.push(item.value);
+ } else {
+ const idx = opts.selected.indexOf(item.value);
+ if (idx >= 0) opts.selected.splice(idx, 1);
+ }
+ opts.onChange(opts.selected);
+ });
+ grid.appendChild(label);
+ }
+ row.appendChild(grid);
+ return row;
+}
+
+// ── Double Ping Display (Krunker shows half the actual ping) ──
+let _doublePingObserver: MutationObserver | null = null;
+
+function initDoublePing(): void {
+ function attach(pingEl: HTMLElement): void {
+ _doublePingObserver = new MutationObserver(() => {
+ const text = pingEl.textContent;
+ if (!text) return;
+ const match = text.match(/(\d+)/);
+ if (!match) return;
+ const doubled = parseInt(match[1]) * 2;
+ _doublePingObserver!.disconnect();
+ pingEl.textContent = text.replace(match[1], String(doubled));
+ _doublePingObserver!.observe(pingEl, { childList: true, characterData: true, subtree: true });
+ });
+ _doublePingObserver.observe(pingEl, { childList: true, characterData: true, subtree: true });
+ }
+
+ const el = document.getElementById('pingText');
+ if (el) { attach(el); return; }
+
+ let attempts = 0;
+ const poll = setInterval(() => {
+ if (++attempts > 60) { clearInterval(poll); return; }
+ const pingEl = document.getElementById('pingText');
+ if (pingEl) { clearInterval(poll); attach(pingEl); }
+ }, 500);
+}
+
+// ── Show Ping in Player List (numeric ms instead of icon) ──
+// genList returns an HTML string — parse it, replace icon elements, return modified HTML.
+function initShowPing(): void {
+ const w = window as any;
+ let attempts = 0;
+ const poll = setInterval(() => {
+ const origGenList = w.windows?.[22]?.genList;
+ if (origGenList && !origGenList.__kpcPingPatched) {
+ clearInterval(poll);
+ const patched = function (this: any) {
+ const html = origGenList.call(this);
+ const parser = new DOMParser();
+ const doc = parser.parseFromString(html, 'text/html');
+ for (const icon of doc.querySelectorAll('.pListPing.material-icons')) {
+ const ping = icon.getAttribute('title');
+ icon.classList.remove('pListPing', 'material-icons');
+ icon.removeAttribute('title');
+ icon.textContent = ping ? ping + ' ' : 'N/A ';
+ }
+ return doc.body.innerHTML;
+ };
+ (patched as any).__kpcPingPatched = true;
+ w.windows[22].genList = patched;
+ } else if (++attempts > 75) {
+ clearInterval(poll);
+ }
+ }, 200);
+}
+
+function hookSettings(): void {
+ const w = window as any;
+ const settingsWindow = w.windows[0];
+ let selectedTab: number = settingsWindow.tabIndex;
+
+ function isClientTab(): boolean {
+ const tabs = settingsWindow.tabs[settingsWindow.settingType];
+ return tabs && selectedTab === tabs.length - 1;
+ }
+
+ function safeRender(): void {
+ if (isClientTab()) renderSettings();
+ }
+
+ const origShowWindow = w.showWindow.bind(w);
+ const origChangeTab = settingsWindow.changeTab.bind(settingsWindow);
+ const origSearchList = settingsWindow.searchList.bind(settingsWindow);
+
+ w.showWindow = (...args: unknown[]) => {
+ const result = origShowWindow(...args);
+ if (args[0] === 1) {
+ if (settingsWindow.settingType === 'basic') {
+ settingsWindow.toggleType({ checked: true });
+ }
+ const advSlider = document.querySelector('.advancedSwitch input#typeBtn') as HTMLInputElement | null;
+ if (advSlider) {
+ advSlider.disabled = true;
+ if (advSlider.nextElementSibling) {
+ advSlider.nextElementSibling.setAttribute('title', 'Client auto-enables advanced settings mode');
+ }
+ }
+
+ const searchInput = document.getElementById('settSearch') as HTMLInputElement | null;
+ const searchQuery = searchInput?.value?.trim() ?? '';
+ if (searchQuery.length > 0) renderSettings(searchQuery);
+ else if (isClientTab()) renderSettings();
+ }
+ return result;
+ };
+
+ settingsWindow.changeTab = (...args: unknown[]) => {
+ const result = origChangeTab(...args);
+ selectedTab = settingsWindow.tabIndex;
+ safeRender();
+ return result;
+ };
+
+ settingsWindow.searchList = (...args: unknown[]) => {
+ const result = origSearchList(...args);
+ const searchInput = document.getElementById('settSearch') as HTMLInputElement | null;
+ const query = searchInput?.value?.trim() ?? '';
+ if (query.length > 0) {
+ renderSettings(query);
+ } else {
+ const existing = document.querySelector('#settHolder .kpc-settings');
+ if (existing && !isClientTab()) existing.remove();
+ else if (isClientTab()) renderSettings();
+ }
+ return result;
+ };
+
+ safeRender();
+}
+
+function createSection(title: string, collapsed?: boolean): { section: HTMLElement; body: HTMLElement } {
+ const section = document.createElement('div');
+ const header = document.createElement('div');
+ header.className = 'setHed';
+ header.innerHTML = '' + (collapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down') + '' + title;
+ const body = document.createElement('div');
+ body.className = 'setBodH' + (collapsed ? ' setting-category-collapsed' : '');
+ header.addEventListener('click', () => {
+ const isCollapsed = body.classList.toggle('setting-category-collapsed');
+ const arrow = header.querySelector('.plusOrMinus');
+ if (arrow) arrow.textContent = isCollapsed ? 'keyboard_arrow_right' : 'keyboard_arrow_down';
+ });
+ section.appendChild(header);
+ section.appendChild(body);
+ return { section, body };
+}
+
+// ── Settings section builders ──
+
+interface SettingsBag {
+ binds: Record;
+ saveBinds: () => void;
+ isWindows: boolean;
+}
+
+function buildGeneralSection(
+ body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag,
+): void {
+ const perfDefaults = { fpsUnlocked: true };
+ const perf = { ...perfDefaults, ...perfConf };
+
+ body.appendChild(createToggleRow({
+ label: 'Unlimited FPS',
+ desc: 'Uncap the frame rate (requires restart)',
+ checked: perf.fpsUnlocked, restart: true,
+ onChange: (v) => { perf.fpsUnlocked = v; ipcRenderer.invoke('set-config', 'performance', perf); },
+ }));
+
+ const gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window' };
+ const game = { ...gameDefaults, ...gameConf };
+
+ body.appendChild(createSelectRow({
+ label: 'Social/Hub Tab Behaviour',
+ desc: 'How social, market, and editor pages open when clicked',
+ options: [{ value: 'New Window', label: 'Tabs (Separate Window)' }, { value: 'Same Window', label: 'Tabs (Overlay Game)' }],
+ value: game.socialTabBehaviour, instant: true,
+ onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
+ }));
+
+ const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
+ const ui = { ...uiDefaults, ...uiConfRaw };
+
+ function saveUI(): void {
+ ipcRenderer.invoke('set-config', 'ui', ui);
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Show Exit Button',
+ desc: 'Show the exit button in the game sidebar',
+ checked: ui.showExitButton, instant: true,
+ onChange: (v) => {
+ ui.showExitButton = v; saveUI();
+ const btn = document.getElementById('clientExit');
+ if (btn) btn.style.display = v ? 'flex' : 'none';
+ },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Block Death Screen Animation',
+ desc: 'Disable the slide-in animation on the death screen',
+ checked: ui.deathscreenAnimation, instant: true,
+ onChange: (v) => { ui.deathscreenAnimation = v; saveUI(); setDeathAnimBlock(v); },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Hide Menu Popups',
+ desc: 'Hide promotional notifications, offers, and streams on the main menu',
+ checked: ui.hideMenuPopups, instant: true,
+ onChange: (v) => {
+ ui.hideMenuPopups = v; saveUI();
+ if (v) startHidePopups(); else stopHidePopups();
+ },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Join as Spectator',
+ desc: 'Automatically enable spectate mode when joining a game',
+ checked: game.joinAsSpectator, instant: true,
+ onChange: (v) => { game.joinAsSpectator = v; ipcRenderer.invoke('set-config', 'game', game); },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Menu Timer',
+ desc: 'Show the game/spectate timer on the menu screen',
+ checked: ui.menuTimer ?? true, instant: true,
+ onChange: (v) => { ui.menuTimer = v; saveUI(); setMenuTimer(v); },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Double Ping Display',
+ desc: 'Show the real ping value (Krunker displays half the actual latency)',
+ checked: ui.doublePing ?? true, refreshOnly: true,
+ onChange: (v) => { ui.doublePing = v; saveUI(); },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Show Ping in Player List',
+ desc: 'Replace the ping icon with numeric millisecond values in the player list',
+ checked: game.showPing ?? true, refreshOnly: true,
+ onChange: (v) => { game.showPing = v; ipcRenderer.invoke('set-config', 'game', game); },
+ }));
+
+ if (bag.isWindows) {
+ body.appendChild(createToggleRow({
+ label: 'Raw Input',
+ desc: 'Bypass OS mouse acceleration for direct 1:1 sensor input (Windows only)',
+ checked: game.rawInput ?? true, refreshOnly: true,
+ onChange: (v) => { game.rawInput = v; ipcRenderer.invoke('set-config', 'game', game); },
+ }));
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Hardpoint Enemy Counter',
+ desc: 'Show enemy capture points in Hardpoint mode',
+ checked: game.hpEnemyCounter ?? true, refreshOnly: true,
+ onChange: (v) => {
+ game.hpEnemyCounter = v; ipcRenderer.invoke('set-config', 'game', game);
+ if (v) initHPCounter(); else destroyHPCounter();
+ },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Show Changelog',
+ desc: 'Show release notes popup when the client updates',
+ checked: ui.showChangelog ?? true, instant: true,
+ onChange: (v) => { ui.showChangelog = v; saveUI(); },
+ }));
+
+ if (ui.deathscreenAnimation) setDeathAnimBlock(true);
+ if (ui.menuTimer ?? true) setMenuTimer(true);
+ if (ui.hideMenuPopups) startHidePopups();
+
+ body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
+ bag.binds.fullscreenToggle = b;
+ bag.saveBinds();
+ }, undefined, true));
+}
+
+function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any): void {
+ const swapEnabled = swapperConf ? swapperConf.enabled : true;
+ const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
+
+ function saveUI(): void {
+ ipcRenderer.invoke('set-config', 'ui', ui);
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Resource Swapper',
+ desc: 'Replace game textures, sounds, and models with local files',
+ checked: swapEnabled,
+ restart: true,
+ onChange: (v) => {
+ ipcRenderer.invoke('get-config', 'swapper').then((conf: any) => {
+ ipcRenderer.invoke('set-config', 'swapper', { enabled: v, path: conf ? conf.path : '' });
+ });
+ },
+ }));
+
+ const folderRow = document.createElement('div');
+ folderRow.className = 'setting settName safety-0 has-button';
+ folderRow.innerHTML =
+ 'Swapper Folder' +
+ 'Place replacement assets here (textures/, sound/, models/)
';
+ const swapFolderBtn = document.createElement('div');
+ swapFolderBtn.className = 'settingsBtn';
+ swapFolderBtn.title = 'Open Folder';
+ swapFolderBtn.innerHTML = 'folder Swapper';
+ swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
+ folderRow.appendChild(swapFolderBtn);
+ body.appendChild(folderRow);
+
+ // ── CSS Theme selector (populated from swap/themes/) ──
+ const themeRow = document.createElement('div');
+ themeRow.className = 'setting settName safety-0 sel has-button';
+ themeRow.innerHTML =
+ 'CSS Theme' +
+ 'Load a custom CSS theme from swap/themes/
';
+ const themeSelect = document.createElement('select');
+ themeSelect.className = 's-update inputGrey2';
+ themeSelect.innerHTML = '';
+ themeRow.appendChild(themeSelect);
+ const themeFolderBtn = document.createElement('div');
+ themeFolderBtn.className = 'settingsBtn';
+ themeFolderBtn.title = 'Open Themes Folder';
+ themeFolderBtn.innerHTML = 'folder';
+ themeFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-themes-folder'));
+ themeRow.appendChild(themeFolderBtn);
+ body.appendChild(themeRow);
+
+ ipcRenderer.invoke('list-themes').then((themes: Array<{ id: string; label: string }>) => {
+ themeSelect.innerHTML = '';
+ for (const t of themes) {
+ const opt = document.createElement('option');
+ opt.value = t.id;
+ opt.textContent = t.label;
+ if (t.id === ui.cssTheme) opt.selected = true;
+ themeSelect.appendChild(opt);
+ }
+ });
+
+ themeSelect.addEventListener('change', () => {
+ ui.cssTheme = themeSelect.value;
+ saveUI();
+ onSettingChanged('refresh');
+ });
+
+ // ── Loading Screen Background ──
+ const bgRow = document.createElement('div');
+ bgRow.className = 'setting settName safety-0 sel has-button';
+ bgRow.innerHTML =
+ 'Loading Background' +
+ 'Custom background image for the loading screen (swap/backgrounds/)
';
+ const bgSelect = document.createElement('select');
+ bgSelect.className = 's-update inputGrey2';
+ bgSelect.innerHTML = '';
+ bgRow.appendChild(bgSelect);
+ const bgFolderBtn = document.createElement('div');
+ bgFolderBtn.className = 'settingsBtn';
+ bgFolderBtn.title = 'Open Backgrounds Folder';
+ bgFolderBtn.innerHTML = 'folder';
+ bgFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-backgrounds-folder'));
+ bgRow.appendChild(bgFolderBtn);
+ body.appendChild(bgRow);
+
+ ipcRenderer.invoke('list-loading-themes').then((themes: Array<{ id: string; label: string }>) => {
+ bgSelect.innerHTML = '';
+ for (const t of themes) {
+ const opt = document.createElement('option');
+ opt.value = t.id;
+ opt.textContent = t.label;
+ if (t.id === ui.loadingTheme) opt.selected = true;
+ bgSelect.appendChild(opt);
+ }
+ });
+
+ bgSelect.addEventListener('change', () => {
+ ui.loadingTheme = bgSelect.value;
+ saveUI();
+ onSettingChanged('refresh');
+ });
+}
+
+function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void {
+ 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);
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Custom Matchmaker',
+ desc: 'Use the matchmaker hotkey to find a game matching your criteria',
+ checked: mm.enabled, instant: true,
+ onChange: (v) => { mm.enabled = v; saveMM(); },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Open Server Browser on Cancel',
+ desc: 'Opens the server browser when no game is found and you cancel',
+ checked: mm.openServerBrowser, instant: true,
+ 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();
+ }, undefined, true));
+ body.appendChild(createKeybindRow('Matchmaker Accept', 'Key to accept a found game', bag.binds.matchmakerAccept, (b) => {
+ bag.binds.matchmakerAccept = b;
+ bag.saveBinds();
+ }, undefined, true));
+ body.appendChild(createKeybindRow('Matchmaker Cancel', 'Key to dismiss the matchmaker popup', bag.binds.matchmakerCancel, (b) => {
+ bag.binds.matchmakerCancel = b;
+ bag.saveBinds();
+ }, undefined, true));
+
+ body.appendChild(createNumberRow({
+ label: 'Min Players', desc: 'Minimum player count in lobby (0-7)',
+ min: 0, max: 7, value: mm.minPlayers, instant: true,
+ onChange: (v) => { mm.minPlayers = v; saveMM(); },
+ }));
+
+ body.appendChild(createNumberRow({
+ label: 'Max Players', desc: 'Maximum player count in lobby (0-7)',
+ min: 0, max: 7, value: mm.maxPlayers, instant: true,
+ onChange: (v) => { mm.maxPlayers = v; saveMM(); },
+ }));
+
+ body.appendChild(createNumberRow({
+ label: 'Min Remaining Time', desc: 'Minimum seconds remaining in match (0-480)',
+ min: 0, max: 480, value: mm.minRemainingTime, instant: true,
+ onChange: (v) => { mm.minRemainingTime = v; saveMM(); },
+ }));
+
+ body.appendChild(createCheckboxGrid({
+ header: 'Regions (none selected = all)',
+ items: MATCHMAKER_REGIONS.map(r => ({ value: r, label: MATCHMAKER_REGION_NAMES[r] || r })),
+ selected: mm.regions,
+ onChange: () => saveMM(),
+ }));
+
+ body.appendChild(createCheckboxGrid({
+ header: 'Gamemodes (none selected = all)',
+ items: MATCHMAKER_GAMEMODE_FILTER.map(gm => ({ value: gm, label: gm })),
+ selected: mm.gamemodes,
+ onChange: () => saveMM(),
+ }));
+
+ if (!mm.maps) mm.maps = [];
+ body.appendChild(createCheckboxGrid({
+ header: 'Maps (none selected = all)',
+ items: MATCHMAKER_MAP_FILTER.map(m => ({ value: m, label: MATCHMAKER_MAP_NAMES[m] || m })),
+ selected: mm.maps,
+ onChange: () => saveMM(),
+ }));
+}
+
+function buildDiscordSection(body: HTMLElement, discordConf: any): void {
+ const discord = { enabled: false, ...discordConf };
+
+ body.appendChild(createToggleRow({
+ label: 'Discord Rich Presence',
+ desc: 'Show game activity in your Discord profile',
+ checked: discord.enabled,
+ restart: true,
+ onChange: (v) => {
+ discord.enabled = v;
+ ipcRenderer.invoke('set-config', 'discord', discord);
+ },
+ }));
+}
+
+// ── Alt Manager helpers ──
+function switchToAccount(account: { username: string; password: string }): void {
+ const w = window as any;
+ if (typeof w.loginOrRegister !== 'function') return;
+
+ function doLogin(): void {
+ w.loginOrRegister();
+ queueMicrotask(() => {
+ const toggleBtn = document.querySelector('.auth-toggle-btn') as HTMLElement;
+ if (toggleBtn && toggleBtn.textContent?.includes('username')) toggleBtn.click();
+ queueMicrotask(() => {
+ const nameInput = document.querySelector('#accName') as HTMLInputElement;
+ const passInput = document.querySelector('#accPass') as HTMLInputElement;
+ if (!nameInput || !passInput) return;
+ nameInput.value = account.username;
+ passInput.value = account.password;
+ nameInput.dispatchEvent(new Event('input', { bubbles: true }));
+ passInput.dispatchEvent(new Event('input', { bubbles: true }));
+ const submitBtn = document.querySelector('.io-button') as HTMLElement;
+ if (submitBtn) submitBtn.click();
+ });
+ });
+ }
+
+ if (typeof w.logoutAcc === 'function') {
+ w.logoutAcc();
+ setTimeout(doLogin, 500);
+ } else {
+ doLogin();
+ }
+}
+
+function buildAccountsSection(body: HTMLElement, accountsArr: any[]): void {
+ const accounts: any[] = accountsArr || [];
+
+ const addBtn = document.createElement('div');
+ addBtn.className = 'setting settName safety-0 has-button';
+ addBtn.innerHTML =
+ 'Add Account' +
+ '' +
+ 'Save a Krunker account for quick switching
';
+ body.appendChild(addBtn);
+
+ const form = document.createElement('div');
+ form.className = 'kpc-acc-form';
+ form.style.display = 'none';
+ form.innerHTML =
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '' +
+ '
';
+ body.appendChild(form);
+
+ const labelIn = form.querySelector('.kpc-acc-label') as HTMLInputElement;
+ const userIn = form.querySelector('.kpc-acc-user') as HTMLInputElement;
+ const passIn = form.querySelector('.kpc-acc-pass') as HTMLInputElement;
+
+ // Stop Krunker's global keydown handler from eating keystrokes in our inputs
+ form.querySelectorAll('input').forEach(input => {
+ input.addEventListener('keydown', (e) => e.stopPropagation());
+ });
+
+ addBtn.querySelector('button')!.addEventListener('click', () => {
+ form.style.display = form.style.display === 'none' ? '' : 'none';
+ });
+
+ form.querySelector('.kpc-acc-cancel')!.addEventListener('click', () => {
+ form.style.display = 'none';
+ });
+
+ const listEl = document.createElement('div');
+ body.appendChild(listEl);
+
+ function renderList(): void {
+ listEl.innerHTML = '';
+ if (accounts.length === 0) {
+ listEl.innerHTML = 'No saved accounts
';
+ return;
+ }
+ accounts.forEach((acc, i) => {
+ const row = document.createElement('div');
+ row.className = 'kpc-acc-item';
+ row.innerHTML =
+ '' +
+ '' + escapeHtml(acc.label) + '' +
+ '
' +
+ '' +
+ '' +
+ '' +
+ '
';
+ row.querySelector('.kpc-acc-switch')!.addEventListener('click', () => {
+ ipcRenderer.invoke('alt-get-credentials', i).then((creds: { username: string; password: string } | null) => {
+ if (creds) switchToAccount(creds);
+ });
+ });
+ row.querySelector('.kpc-acc-delete')!.addEventListener('click', () => {
+ ipcRenderer.invoke('alt-remove', i).then(() => {
+ accounts.splice(i, 1);
+ renderList();
+ });
+ });
+ listEl.appendChild(row);
+ });
+ }
+ renderList();
+
+ form.querySelector('.kpc-acc-save')!.addEventListener('click', () => {
+ const label = labelIn.value.trim();
+ const user = userIn.value.trim();
+ const pass = passIn.value;
+ if (!label || !user || !pass) return;
+ const newAcc = { label, username: user, password: pass };
+ ipcRenderer.invoke('alt-save', newAcc).then(() => {
+ accounts.push({ label });
+ labelIn.value = '';
+ userIn.value = '';
+ passIn.value = '';
+ form.style.display = 'none';
+ renderList();
+ });
+ });
+}
+
+function buildChatSection(body: HTMLElement, gameConf: any, translatorConf: any): void {
+ const game = { betterChat: true, chatHistorySize: 200, ...gameConf };
+
+ function saveGame(): void {
+ ipcRenderer.invoke('set-config', 'game', game);
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Better Chat',
+ desc: 'Merge team and all-chat with colored [T]/[M] prefixes',
+ checked: game.betterChat, instant: true,
+ onChange: (v) => { game.betterChat = v; saveGame(); setBetterChat(v); },
+ }));
+
+ body.appendChild(createNumberRow({
+ label: 'Chat History Size', desc: 'Maximum chat messages to keep (0 to disable history preservation)',
+ min: 0, max: 1000, value: game.chatHistorySize, instant: true,
+ onChange: (v) => { game.chatHistorySize = v; saveGame(); setChatHistorySize(v); },
+ }));
+
+ // Translator settings inline
+ const tl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
+
+ function saveTL(): void {
+ ipcRenderer.invoke('set-config', 'translator', tl);
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Chat Translator',
+ desc: 'Automatically translate non-English chat messages',
+ checked: tl.enabled, instant: true,
+ onChange: (v) => {
+ tl.enabled = v;
+ saveTL();
+ updateTranslatorConfig({ enabled: v });
+ },
+ }));
+
+ body.appendChild(createSelectRow({
+ label: 'Target Language',
+ desc: 'Language to translate messages into', instant: true,
+ options: [
+ { value: 'en', label: 'English' },
+ { value: 'es', label: 'Spanish' },
+ { value: 'fr', label: 'French' },
+ { value: 'de', label: 'German' },
+ { value: 'pt', label: 'Portuguese' },
+ { value: 'ru', label: 'Russian' },
+ { value: 'ja', label: 'Japanese' },
+ { value: 'ko', label: 'Korean' },
+ { value: 'zh', label: 'Chinese' },
+ { value: 'ar', label: 'Arabic' },
+ { value: 'hi', label: 'Hindi' },
+ { value: 'tr', label: 'Turkish' },
+ { value: 'pl', label: 'Polish' },
+ { value: 'it', label: 'Italian' },
+ { value: 'nl', label: 'Dutch' },
+ ],
+ value: tl.targetLanguage,
+ onChange: (v) => {
+ tl.targetLanguage = v;
+ saveTL();
+ updateTranslatorConfig({ targetLanguage: v });
+ },
+ }));
+
+ body.appendChild(createToggleRow({
+ label: 'Show Language Tag',
+ desc: 'Show detected language code before translations (e.g. [FR])',
+ checked: tl.showLanguageTag, instant: true,
+ onChange: (v) => {
+ tl.showLanguageTag = v;
+ saveTL();
+ updateTranslatorConfig({ showLanguageTag: v });
+ },
+ }));
+}
+
+function buildAdvancedSection(
+ body: HTMLElement, advConf: any, perfConf: any, isWindows: boolean,
+): void {
+ const advDefaults = {
+ removeUselessFeatures: true,
+ gpuRasterizing: false,
+ helpfulFlags: true,
+ increaseLimits: false,
+ lowLatency: false,
+ experimentalFlags: false,
+ angleBackend: 'default',
+ verboseLogging: false,
+ };
+ const adv = { ...advDefaults, ...advConf };
+ const perf = { cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
+
+ function savePerf(): void {
+ ipcRenderer.invoke('set-config', 'performance', perf);
+ }
+
+ function saveAdv(): void {
+ ipcRenderer.invoke('set-config', 'advanced', adv);
+ }
+
+ const angleOptions: Array<{ value: string; label: string }> = isWindows
+ ? [
+ { value: 'default', label: 'Default (D3D11)' },
+ { value: 'gl', label: 'OpenGL' },
+ { value: 'd3d9', label: 'Direct3D 9' },
+ { value: 'd3d11', label: 'Direct3D 11' },
+ { value: 'd3d11on12', label: 'D3D11on12' },
+ { value: 'vulkan', label: 'Vulkan' },
+ ]
+ : [
+ { value: 'default', label: 'Default' },
+ { value: 'gl', label: 'OpenGL' },
+ { value: 'vulkan', label: 'Vulkan' },
+ ];
+
+ body.appendChild(createSelectRow({
+ label: 'ANGLE Backend',
+ desc: 'Graphics API used for WebGL rendering',
+ options: angleOptions,
+ value: adv.angleBackend, restart: true,
+ onChange: (v) => { adv.angleBackend = v; saveAdv(); },
+ }));
+
+ const advToggles: Array<{ key: string; label: string; desc: string; safety: number }> = [
+ { key: 'removeUselessFeatures', label: 'Remove Useless Features', desc: 'Disables crash reporting, metrics, print preview, and other unused Chromium features', safety: 1 },
+ { key: 'gpuRasterizing', label: 'GPU Rasterization', desc: 'Force GPU rasterization and out-of-process rasterization', safety: 2 },
+ { key: 'helpfulFlags', label: 'Useful Flags', desc: 'Enables WebGL, JS harmony, V8 features, background throttle prevention, and autoplay bypass', safety: 3 },
+ { key: 'increaseLimits', label: 'Increase Limits', desc: 'Raises renderer process, WebGL context, and WebRTC CPU limits; ignores GPU blocklist', safety: 4 },
+ { key: 'lowLatency', label: 'Low Latency Flags', desc: 'Enables high-resolution timer, QUIC protocol, and high-performance GPU', safety: 4 },
+ { key: 'experimentalFlags', label: 'Experimental Flags', desc: 'Enables accelerated video decode, native GPU memory buffers, high DPI support, and disables pings/proxy', safety: 4 },
+ ];
+
+ for (const t of advToggles) {
+ body.appendChild(createToggleRow({
+ label: t.label, desc: t.desc,
+ checked: !!adv[t.key], restart: true,
+ safety: t.safety,
+ onChange: (v) => { adv[t.key] = v; saveAdv(); },
+ }));
+ }
+
+ body.appendChild(createNumberRow({
+ label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
+ min: 1, max: 3, value: perf.cpuThrottleGame, instant: true, safety: 2,
+ onChange: (v) => { perf.cpuThrottleGame = v; savePerf(); },
+ }));
+
+ body.appendChild(createNumberRow({
+ label: 'CPU Throttle (Menu)', desc: 'CPU throttle rate on menu screens (1 = no throttle, 3 = heavy throttle)',
+ min: 1, max: 3, value: perf.cpuThrottleMenu, instant: true, safety: 1,
+ onChange: (v) => { perf.cpuThrottleMenu = v; savePerf(); },
+ }));
+
+ if (isWindows) {
+ body.appendChild(createSelectRow({
+ label: 'Process Priority',
+ desc: 'OS-level process priority for the client (Windows only)',
+ options: [
+ { value: 'Normal', label: 'Normal' },
+ { value: 'Above Normal', label: 'Above Normal' },
+ { value: 'High', label: 'High' },
+ { value: 'Below Normal', label: 'Below Normal' },
+ { value: 'Low', label: 'Low' },
+ ],
+ value: perf.processPriority, restart: true, safety: 2,
+ onChange: (v) => { perf.processPriority = v; savePerf(); },
+ }));
+ }
+
+ body.appendChild(createToggleRow({
+ label: 'Verbose Logging',
+ desc: 'Forward all preload console output to the Electron log file',
+ checked: adv.verboseLogging, instant: true,
+ onChange: (v) => {
+ adv.verboseLogging = v; saveAdv();
+ _verboseLogging = v;
+ },
+ }));
+}
+
+// ── Search filter + "no settings" cleanup ──
+function applySearchFilter(container: HTMLElement, holder: HTMLElement, searchQuery: string): void {
+ const query = searchQuery.toLowerCase();
+ const sections = Array.from(container.children).filter(el => el.querySelector('.setHed'));
+ sections.forEach(sectionEl => {
+ const sectionTitle = sectionEl.querySelector('.setHed')?.textContent?.toLowerCase() || '';
+ const body = sectionEl.querySelector('.setBodH');
+ if (!body) { (sectionEl as HTMLElement).style.display = 'none'; return; }
+
+ if (sectionTitle.includes(query)) {
+ body.classList.remove('setting-category-collapsed');
+ return;
+ }
+
+ let visibleCount = 0;
+ Array.from(body.children).forEach(child => {
+ const el = child as HTMLElement;
+ const text = el.textContent?.toLowerCase() || '';
+ if (text.includes(query)) {
+ el.style.display = '';
+ visibleCount++;
+ } else {
+ el.style.display = 'none';
+ }
+ });
+ if (visibleCount === 0) {
+ (sectionEl as HTMLElement).style.display = 'none';
+ } else {
+ body.classList.remove('setting-category-collapsed');
+ }
+ });
+
+ const hasVisible = sections.find(el => (el as HTMLElement).style.display !== 'none');
+ if (hasVisible) {
+ Array.from(holder.children).forEach(child => {
+ if ((child as HTMLElement).textContent?.toLowerCase().includes('no settings')) {
+ (child as HTMLElement).remove();
+ }
+ });
+ }
+}
+
+function renderSettings(searchQuery?: string): void {
+ const holder = document.getElementById('settHolder');
+ if (!holder) return;
+
+ refreshLevel = RefreshLevel.none;
+ if (refreshPopupEl) { refreshPopupEl.remove(); refreshPopupEl = null; }
+
+ if (searchQuery) {
+ const existing = holder.querySelector('.kpc-settings');
+ if (existing) existing.remove();
+ } else {
+ while (holder.firstChild) holder.removeChild(holder.firstChild);
+ }
+
+ const container = document.createElement('div');
+ container.className = 'kpc-settings';
+
+ // ── Action button grid ──
+ const actionGrid = document.createElement('div');
+ actionGrid.className = 'kpc-action-grid';
+
+ const actionButtons: Array<{ label: string; color: string; full?: boolean; action: () => void }> = [
+ { label: 'Open Resource Swapper', color: 'kpc-ab-pink', action: () => ipcRenderer.invoke('open-swap-folder') },
+ { label: 'Reset Resource Swapper', color: 'kpc-ab-pink', action: () => {
+ if (confirm('Reset resource swapper? This will delete all files in the swapper folder.')) {
+ ipcRenderer.invoke('reset-swapper');
+ }
+ }},
+ { label: 'Open Electron Logs', color: 'kpc-ab-red', action: () => ipcRenderer.invoke('open-electron-log') },
+ { label: 'Restart Client', color: 'kpc-ab-orange', full: true, action: () => ipcRenderer.invoke('restart-client') },
+ { label: 'Reset Options', color: 'kpc-ab-red', action: () => {
+ if (confirm('Reset all settings to defaults? The client will restart.')) {
+ ipcRenderer.invoke('reset-options');
+ }
+ }},
+ { label: 'Delete All Data', color: 'kpc-ab-red', action: () => {
+ if (confirm('Delete all data (config, logs)? Scripts are preserved. The client will restart.')) {
+ ipcRenderer.invoke('delete-all-data');
+ }
+ }},
+ ];
+
+ for (const ab of actionButtons) {
+ const btn = document.createElement('button');
+ btn.className = 'kpc-action-btn ' + ab.color + (ab.full ? ' full' : '');
+ btn.textContent = ab.label;
+ btn.addEventListener('click', ab.action);
+ actionGrid.appendChild(btn);
+ }
+ container.appendChild(actionGrid);
+
+ // ── Create section shells ──
+ const genSec = createSection('General');
+ container.appendChild(genSec.section);
+ const swapSec = createSection('Swapper');
+ container.appendChild(swapSec.section);
+ const mmSec = createSection('Matchmaker');
+ container.appendChild(mmSec.section);
+ const chatSec = createSection('Chat');
+ container.appendChild(chatSec.section);
+ const discordSec = createSection('Discord');
+ container.appendChild(discordSec.section);
+ const accSec = createSection('Accounts', true);
+ container.appendChild(accSec.section);
+ const advSec = createSection('Advanced');
+ container.appendChild(advSec.section);
+ const usSec = createSection('Userscripts');
+ container.appendChild(usSec.section);
+
+ // Load all configs in a single IPC call + platform info
+ Promise.all([
+ ipcRenderer.invoke('get-all-config', ['swapper', 'matchmaker', 'keybinds', 'advanced', 'game', 'ui', 'discord', 'translator', 'accounts', 'performance']),
+ ipcRenderer.invoke('get-platform'),
+ ]).then(([allConf, platformInfo]: [any, any]) => {
+ const swapperConf = allConf.swapper;
+ const mmConf = allConf.matchmaker;
+ const keybindsConf = allConf.keybinds;
+ const advConf = allConf.advanced;
+ const gameConf = allConf.game;
+ const uiConfRaw = allConf.ui;
+ const discordConf = allConf.discord;
+ const translatorConf = allConf.translator;
+ const defaultBinds = {
+ matchmaker: { key: 'F6', ctrl: false, shift: false, alt: false },
+ matchmakerAccept: { key: 'Enter', ctrl: false, shift: false, alt: false },
+ matchmakerCancel: { key: 'Escape', ctrl: false, shift: false, alt: false },
+ fullscreenToggle: { key: 'F11', ctrl: false, shift: false, alt: false },
+ };
+ const binds = { ...defaultBinds, ...keybindsConf };
+ const isWindows = platformInfo && platformInfo.isWindows;
+
+ const bag: SettingsBag = {
+ binds,
+ saveBinds: () => ipcRenderer.invoke('set-config', 'keybinds', binds),
+ isWindows,
+ };
+
+ // Populate each section
+ buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag);
+ buildSwapperSection(swapSec.body, swapperConf, uiConfRaw);
+ buildMatchmakerSection(mmSec.body, mmConf, bag);
+ buildChatSection(chatSec.body, gameConf, translatorConf);
+ buildDiscordSection(discordSec.body, discordConf);
+ buildAccountsSection(accSec.body, allConf.accounts);
+ buildAdvancedSection(advSec.body, advConf, allConf.performance, isWindows);
+ renderUserscriptsSection(usSec.body);
+
+ if (searchQuery) applySearchFilter(container, holder, searchQuery);
+
+ holder.appendChild(container);
+ }).catch((err: any) => {
+ console.error('[KCC] Settings render error:', err);
+ });
+}
+
+// ── Userscripts settings section ──
+function renderUserscriptsSection(body: HTMLElement): void {
+ ipcRenderer.invoke('get-config', 'userscripts').then((usConf: any) => {
+ const us = usConf || { enabled: true, path: '' };
+
+ body.appendChild(createToggleRow({
+ label: 'Userscripts',
+ desc: 'Load custom scripts from the scripts folder',
+ checked: us.enabled, restart: true,
+ onChange: (v) => { us.enabled = v; ipcRenderer.invoke('set-config', 'userscripts', us); },
+ }));
+
+ const usFolderRow = document.createElement('div');
+ usFolderRow.className = 'setting settName safety-0 has-button';
+ usFolderRow.innerHTML =
+ 'Scripts Folder' +
+ 'Place .js userscript files here
';
+ const usFolderBtn = document.createElement('div');
+ usFolderBtn.className = 'settingsBtn';
+ usFolderBtn.title = 'Open Folder';
+ usFolderBtn.innerHTML = 'folder Scripts';
+ usFolderBtn.addEventListener('click', () => ipcRenderer.invoke('userscripts-open-folder'));
+ usFolderRow.appendChild(usFolderBtn);
+ body.appendChild(usFolderRow);
+
+ const scriptInstances = getInstances();
+ if (scriptInstances.length === 0) {
+ const emptyRow = document.createElement('div');
+ emptyRow.className = 'setting settName safety-0';
+ emptyRow.innerHTML =
+ 'No userscripts found. Place .js files in the scripts folder and reload.
';
+ body.appendChild(emptyRow);
+ return;
+ }
+
+ for (const inst of scriptInstances) {
+ const scriptRow = document.createElement('div');
+ scriptRow.className = 'setting settName safety-0 bool';
+
+ const displayName = escapeHtml(inst.meta.name || inst.filename);
+ const metaParts: string[] = [];
+ if (inst.meta.author) metaParts.push('by ' + escapeHtml(inst.meta.author));
+ if (inst.meta.version) metaParts.push('v' + escapeHtml(inst.meta.version));
+ const metaLine = metaParts.length > 0 ? '' + metaParts.join(' · ') + '' : '';
+ const descText = escapeHtml(inst.meta.desc || '');
+
+ scriptRow.innerHTML =
+ '' + displayName + '' +
+ '' +
+ '' + descText + (metaLine ? '
' + metaLine : '') + '
';
+ body.appendChild(scriptRow);
+
+ const cb = scriptRow.querySelector('input[type="checkbox"]') as HTMLInputElement;
+ const settingsContainer = document.createElement('div');
+ settingsContainer.className = 'kpc-us-settings';
+ body.appendChild(settingsContainer);
+
+ if (inst.enabled && inst.settings) {
+ renderScriptSettings(inst, settingsContainer);
+ }
+
+ cb.addEventListener('change', () => {
+ const { needsReload } = setScriptEnabled(inst.filename, cb.checked, _console);
+ settingsContainer.innerHTML = '';
+ if (cb.checked && inst.settings) {
+ renderScriptSettings(inst, settingsContainer);
+ }
+ if (needsReload) {
+ onSettingChanged('refresh');
+ }
+ });
+ }
+ });
+}
+
+function renderScriptSettings(inst: UserscriptInstance, container: HTMLElement): void {
+ if (!inst.settings) return;
+
+ for (const [, setting] of Object.entries(inst.settings)) {
+ const typeClass = setting.type === 'bool' ? 'bool' : setting.type === 'sel' ? 'sel' : setting.type === 'num' ? 'num' : setting.type === 'keybind' ? 'keybind' : '';
+ const row = document.createElement('div');
+ row.className = 'setting settName safety-0' + (typeClass ? ' ' + typeClass : '');
+ row.innerHTML =
+ '' + escapeHtml(setting.title) + '' +
+ (setting.desc ? '' + escapeHtml(setting.desc) + '
' : '');
+
+ switch (setting.type) {
+ case 'bool': {
+ const label = document.createElement('label');
+ label.className = 'switch';
+ label.innerHTML =
+ '' +
+ '';
+ row.appendChild(label);
+ const input = label.querySelector('input') as HTMLInputElement;
+ input.addEventListener('change', () => {
+ setting.value = input.checked;
+ if (typeof setting.changed === 'function') setting.changed(setting.value);
+ saveScriptSetting(inst);
+ });
+ break;
+ }
+ case 'num': {
+ const input = document.createElement('input');
+ input.type = 'number';
+ input.className = 'rb-input s-update sliderVal';
+ input.value = String(setting.value);
+ if (setting.min !== undefined) input.min = String(setting.min);
+ if (setting.max !== undefined) input.max = String(setting.max);
+ if (setting.step !== undefined) input.step = String(setting.step);
+ row.appendChild(input);
+ input.addEventListener('change', () => {
+ setting.value = parseFloat(input.value) || 0;
+ if (typeof setting.changed === 'function') setting.changed(setting.value);
+ saveScriptSetting(inst);
+ });
+ break;
+ }
+ case 'sel': {
+ const select = document.createElement('select');
+ select.className = 's-update inputGrey2';
+ if (setting.opts) {
+ for (const opt of setting.opts) {
+ const option = document.createElement('option');
+ option.value = String(opt);
+ option.textContent = String(opt);
+ if (String(opt) === String(setting.value)) option.selected = true;
+ select.appendChild(option);
+ }
+ }
+ row.appendChild(select);
+ select.addEventListener('change', () => {
+ setting.value = select.value;
+ if (typeof setting.changed === 'function') setting.changed(setting.value);
+ saveScriptSetting(inst);
+ });
+ break;
+ }
+ case 'color': {
+ const input = document.createElement('input');
+ input.type = 'color';
+ input.className = 'kpc-color-input';
+ input.value = String(setting.value) || '#ffffff';
+ row.appendChild(input);
+ input.addEventListener('input', () => {
+ setting.value = input.value;
+ if (typeof setting.changed === 'function') setting.changed(setting.value);
+ saveScriptSetting(inst);
+ });
+ break;
+ }
+ case 'keybind': {
+ const bind = setting.value as Keybind;
+ const keyEl = document.createElement('span');
+ keyEl.className = 'keyIcon kpc-keyIcon';
+ keyEl.textContent = keybindDisplayString(bind);
+ keyEl.addEventListener('click', () => {
+ openKeybindDialog(setting.title).then((newBind) => {
+ setting.value = newBind;
+ keyEl.textContent = keybindDisplayString(newBind);
+ if (typeof setting.changed === 'function') setting.changed(setting.value);
+ saveScriptSetting(inst);
+ });
+ });
+ row.appendChild(keyEl);
+ break;
+ }
+ }
+
+ container.appendChild(row);
+ }
+}
+
+function saveScriptSetting(inst: UserscriptInstance): void {
+ if (!inst.settings) return;
+ const prefs: Record = {};
+ for (const [k, s] of Object.entries(inst.settings)) {
+ prefs[k] = s.value;
+ }
+ ipcRenderer.invoke('userscripts-save-prefs', inst.filename, prefs);
+}
+
+// ── Hide menu popups (polling-based, safe per MutationObserver constraint) ──
+let _hidePopupsInterval: ReturnType | null = null;
+const HIDE_POPUPS_CSS =
+ '#leftTabsHolder > .youNewDiv:not(#battlepassAd), .webpush-container, ' +
+ '#homeStoreAd, #streamContainerNew, #bundlePop, #genericPop.claimPop, ' +
+ '#newsHolder, #streamContainer { display: none !important; }';
+const HIDE_POPUPS_ELS = ['homeStoreAd', 'streamContainerNew'];
+
+function startHidePopups(): void {
+ if (_hidePopupsInterval) return;
+ if (!document.getElementById('kpc-hideMenuPopups')) {
+ const style = document.createElement('style');
+ style.id = 'kpc-hideMenuPopups';
+ style.textContent = HIDE_POPUPS_CSS;
+ document.head.appendChild(style);
+ }
+ const w = window as any;
+ _hidePopupsInterval = setInterval(() => {
+ for (const id of HIDE_POPUPS_ELS) {
+ const el = document.getElementById(id);
+ if (el && el.style.display !== 'none') el.style.display = 'none';
+ }
+ const bundlePop = document.getElementById('bundlePop');
+ if (bundlePop && bundlePop.children.length > 0 && bundlePop.style.display !== 'none') {
+ if (typeof w.clearPops === 'function') w.clearPops();
+ }
+ const genericPop = document.getElementById('genericPop');
+ if (genericPop && genericPop.classList.contains('claimPop') && genericPop.style.display !== 'none') {
+ if (typeof w.clearPops === 'function') w.clearPops();
+ }
+ }, 1000);
+}
+
+function stopHidePopups(): void {
+ if (_hidePopupsInterval) { clearInterval(_hidePopupsInterval); _hidePopupsInterval = null; }
+ const style = document.getElementById('kpc-hideMenuPopups');
+ if (style) style.remove();
+ for (const id of HIDE_POPUPS_ELS) {
+ const el = document.getElementById(id);
+ if (el) el.style.display = '';
+ }
+}
+
+// ── Matchmaker IPC listener ──
+ipcRenderer.on('matchmaker-find', (_e, mmConfig: MatchmakerConfig) => {
+ fetchGame(mmConfig, _console).catch((err) => _console.error('[KCC] Matchmaker error:', err));
+});
+
+
+// ── Wait for main process to signal page load, then poll for settings window ──
+ipcRenderer.on('main_did-finish-load', () => {
+ _console.log('[KCC] did-finish-load received, waiting to hook settings...');
+
+ const isGamePage = window.location.pathname === '/' || window.location.pathname === '';
+
+ // ── Batch all config reads into a single IPC call ──
+ (window as any).closeClient = () => window.close();
+ Promise.all([
+ ipcRenderer.invoke('get-all-config', ['ui', 'userscripts', 'game', 'translator', 'keybinds', 'discord', 'advanced', 'performance']),
+ ipcRenderer.invoke('get-platform'),
+ ipcRenderer.invoke('get-version'),
+ ]).then(([allConf, _platformInfo, currentVersion]: [any, any, string]) => {
+ const uiConf = allConf.ui;
+ const usConf = allConf.userscripts;
+ const gameConf = allConf.game;
+ const translatorConf = allConf.translator;
+ const discordConf = allConf.discord;
+ const advConf = allConf.advanced;
+
+ // ── Verbose logging toggle ──
+ _verboseLogging = advConf?.verboseLogging ?? false;
+
+ // ── Exit button + UI toggles ──
+ const showExit = uiConf ? (uiConf.showExitButton !== false) : true;
+ const showExitBtn = () => {
+ const btn = document.getElementById('clientExit');
+ if (btn) {
+ btn.style.display = showExit ? 'flex' : 'none';
+ return true;
+ }
+ return false;
+ };
+ if (!showExitBtn()) {
+ let exitAttempts = 0;
+ const exitPoll = setInterval(() => {
+ if (showExitBtn() || ++exitAttempts > 30) clearInterval(exitPoll);
+ }, 500);
+ }
+
+ if (uiConf?.deathscreenAnimation) setDeathAnimBlock(true);
+ if (uiConf?.hideMenuPopups) startHidePopups();
+ if (uiConf?.menuTimer ?? true) setMenuTimer(true);
+
+ // ── Double ping display ──
+ if (isGamePage && (uiConf?.doublePing ?? true)) {
+ initDoublePing();
+ }
+
+ // ── Show ping in player list ──
+ if (isGamePage && (gameConf?.showPing ?? true)) {
+ initShowPing();
+ }
+
+ // ── Raw input (Windows only — unadjustedMovement) ──
+ if (isGamePage && process.platform === 'win32' && (gameConf?.rawInput ?? true)) {
+ const origLock = HTMLCanvasElement.prototype.requestPointerLock;
+ HTMLCanvasElement.prototype.requestPointerLock = function (opts?: any) {
+ const promise = origLock.call(this, { ...opts, unadjustedMovement: true }) as any;
+ if (promise && typeof promise.catch === 'function') {
+ return promise.catch(() => origLock.call(this, opts));
+ }
+ return promise;
+ };
+ }
+
+ // ── Better chat + Chat history ──
+ if (isGamePage) {
+ initChat({
+ betterChat: gameConf?.betterChat ?? true,
+ chatHistorySize: gameConf?.chatHistorySize ?? 200,
+ }, _console);
+ }
+
+ // ── Competitive features ──
+ if (isGamePage && (gameConf?.hpEnemyCounter ?? true)) {
+ initHPCounter();
+ }
+ if (isGamePage) {
+ initRankProgress();
+ }
+
+ // ── CPU throttle state notifications ──
+ if (isGamePage) {
+ let inGame = false;
+ setInterval(() => {
+ const uiBase = document.getElementById('uiBase');
+ const nowInGame = !!uiBase && uiBase.className !== 'onMenu' && uiBase.className !== '';
+ if (nowInGame !== inGame) {
+ inGame = nowInGame;
+ ipcRenderer.send('throttle-state', inGame ? 'game' : 'menu');
+ }
+ }, 2000);
+ }
+
+ // ── Changelog popup ──
+ if (isGamePage && (uiConf?.showChangelog ?? true)) {
+ checkChangelog(currentVersion, uiConf?.lastSeenVersion || '');
+ }
+
+ // ── Battle Pass Claim All (game page only) ──
+ // Poll for .bpBotH element — injects button when BP window is visible
+ if (isGamePage) {
+ const getClaimable = () => Array.from(document.querySelectorAll('.bpClaimB')).filter(
+ (el: any) => el.offsetParent !== null && el.textContent?.trim() === 'Claim'
+ );
+ setInterval(() => {
+ const bar = document.querySelector('.bpBotH') as HTMLElement | null;
+ if (!bar || bar.offsetParent === null) return;
+ const existing = document.getElementById('claimAllBtn');
+ if (existing) {
+ // Update state on re-check (rewards may have become claimable)
+ const claimable = getClaimable();
+ if (claimable.length > 0) {
+ existing.textContent = 'Claim All';
+ existing.classList.remove('disabled');
+ } else {
+ existing.textContent = 'Nothing to Claim';
+ existing.classList.add('disabled');
+ }
+ return;
+ }
+ const claimable = getClaimable();
+ const btn = document.createElement('div');
+ btn.className = 'bpBtn skip';
+ btn.id = 'claimAllBtn';
+ btn.style.cssText = 'margin-left: 8px; cursor: pointer; background: #4CAF50;';
+ if (claimable.length > 0) {
+ btn.textContent = 'Claim All';
+ } else {
+ btn.textContent = 'Nothing to Claim';
+ btn.classList.add('disabled');
+ }
+ btn.addEventListener('click', async () => {
+ if (btn.classList.contains('disabled')) return;
+ (window as any).playSelect?.(0.1);
+ const items = getClaimable();
+ if (items.length === 0) return;
+ btn.textContent = 'Claiming...';
+ btn.classList.add('disabled');
+ for (const item of items) {
+ (item as HTMLElement).click();
+ await new Promise(r => setTimeout(r, 200));
+ }
+ const remaining = getClaimable();
+ btn.textContent = remaining.length > 0 ? 'Claim All' : 'Nothing to Claim';
+ btn.classList.toggle('disabled', remaining.length === 0);
+ });
+ bar.appendChild(btn);
+ }, 500);
+ }
+
+ // ── Initialize userscripts ──
+ const usEnabled = usConf ? usConf.enabled : true;
+ if (usEnabled) {
+ initUserscripts(_console).catch(err => _console.error('[KCC] Userscript init error:', err));
+ }
+
+ // ── Join as Spectator — auto-enable spectate on regular game join ──
+ if (isGamePage && gameConf?.joinAsSpectator) {
+ let attempts = 0;
+ const poll = setInterval(() => {
+ if (++attempts > 300) { clearInterval(poll); return; }
+ const uiBase = document.getElementById('uiBase');
+ if (!uiBase || uiBase.className === '') return;
+ if (uiBase.className === 'onMenu') {
+ const specBtn = document.querySelector('#spectButton input') as HTMLInputElement;
+ if (specBtn && !specBtn.checked) {
+ (window as any).setSpect(1);
+ }
+ clearInterval(poll);
+ } else {
+ clearInterval(poll);
+ }
+ }, 100);
+ }
+
+ // ── Initialize chat translator (game page only) ──
+ if (isGamePage) {
+ const mergedTl = { enabled: true, targetLanguage: 'en', showLanguageTag: true, ...translatorConf };
+ initTranslator(_console, mergedTl);
+ }
+
+ // ── Discord Rich Presence game state polling ──
+ if (isGamePage && discordConf?.enabled) {
+ let lastDetails = '';
+ let lastState = '';
+ let gameStartTimestamp = Math.floor(Date.now() / 1000);
+
+ function pollDiscordState(): void {
+ let details: string;
+ let state = '';
+ let startTimestamp: number | undefined = undefined;
+
+ const w = window as any;
+ const spectating = w.spectating;
+
+ let gameActivity: any = null;
+ if (typeof w.getGameActivity === 'function') {
+ try { gameActivity = w.getGameActivity(); } catch { /* game API unavailable */ }
+ }
+
+ if (spectating) {
+ details = 'Spectating';
+ if (gameActivity?.map) {
+ state = gameActivity.map;
+ }
+ } else {
+ const uiBase = document.getElementById('uiBase');
+ if (uiBase && uiBase.className === 'onMenu') {
+ details = 'In Menus';
+ } else {
+ if (gameActivity?.mode && gameActivity?.map) {
+ details = gameActivity.mode + ' on ' + gameActivity.map;
+ } else {
+ const mapInfo = document.getElementById('mapInfo');
+ details = mapInfo?.textContent || 'Playing Krunker';
+ }
+
+ if (gameActivity?.class?.name) {
+ state = gameActivity.class.name;
+ } else {
+ const classElem = document.getElementById('menuClassName');
+ if (classElem?.textContent) state = classElem.textContent;
+ }
+
+ startTimestamp = gameStartTimestamp;
+ }
+ }
+
+ if (details !== lastDetails || state !== lastState) {
+ if (startTimestamp && lastDetails !== details) {
+ gameStartTimestamp = Math.floor(Date.now() / 1000);
+ startTimestamp = gameStartTimestamp;
+ }
+ lastDetails = details;
+ lastState = state;
+ ipcRenderer.send('discord-update', {
+ details,
+ state: state || undefined,
+ startTimestamp,
+ largeImageKey: 'krunker',
+ largeImageText: 'Krunker Civilian Client',
+ });
+ }
+ }
+
+ pollDiscordState();
+ setInterval(pollDiscordState, 5000);
+ document.addEventListener('pointerlockchange', pollDiscordState);
+ }
+ // ── In-game Accounts quick-switch button ──
+ if (isGamePage) {
+ ipcRenderer.invoke('alt-list').then(() => {
+ const altBtn = document.createElement('div');
+ altBtn.id = 'kpcAltBtn';
+ altBtn.className = 'menuItem';
+ altBtn.setAttribute('onmouseenter', 'playTick()');
+ altBtn.innerHTML =
+ 'people' +
+ '';
+
+ function showAltManager(): void {
+ const windowHolder = document.getElementById('windowHolder') as HTMLElement;
+ const menuWindow = document.getElementById('menuWindow') as HTMLElement;
+ const windowHeader = document.getElementById('windowHeader') as HTMLElement;
+ if (!windowHolder || !menuWindow || !windowHeader) return;
+
+ if (windowHolder.style.display !== 'none' && windowHeader.innerText === 'Alt Manager') {
+ windowHolder.style.display = 'none';
+ return;
+ }
+
+ windowHolder.className = 'popupWin';
+ windowHolder.style.display = 'block';
+ menuWindow.classList.value = 'dark';
+ menuWindow.style.cssText = 'width:800px;max-height:calc(100% - 330px);overflow-y:auto;top:50%;transform:translate(-50%,-50%);';
+ windowHeader.innerText = 'Alt Manager';
+
+ function renderAccountList(): void {
+ ipcRenderer.invoke('alt-list').then((accs: any[]) => {
+ let html =
+ 'Alt Manager
' +
+ '
' +
+ 'Add Account
' +
+ '';
+
+ if (!accs || accs.length === 0) {
+ html += '
No saved accounts
';
+ } else {
+ accs.forEach((acc, i) => {
+ html +=
+ '
' +
+ '
' + escapeHtml(acc.label) + '' +
+ '
' +
+ 'login' +
+ '
' +
+ '
' +
+ '
' +
+ 'delete' +
+ '
' +
+ '
';
+ });
+ }
+ html += '
';
+ menuWindow.innerHTML = html;
+
+ const addBtn = document.getElementById('kpcAltAddBtn');
+ if (addBtn) addBtn.addEventListener('click', showAddForm);
+
+ menuWindow.querySelectorAll('.kpc-alt-login').forEach((el) => {
+ el.addEventListener('click', () => {
+ const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
+ if (accs[idx]) {
+ windowHolder.style.display = 'none';
+ ipcRenderer.invoke('alt-get-credentials', idx).then((creds: { username: string; password: string } | null) => {
+ if (creds) switchToAccount(creds);
+ });
+ }
+ });
+ });
+
+ menuWindow.querySelectorAll('.kpc-alt-del').forEach((el) => {
+ el.addEventListener('click', () => {
+ const idx = parseInt((el as HTMLElement).dataset.idx || '0', 10);
+ if (confirm('Delete account "' + (accs[idx]?.label || '') + '"?')) {
+ ipcRenderer.invoke('alt-remove', idx).then(() => renderAccountList());
+ }
+ });
+ });
+ });
+ }
+
+ function showAddForm(): void {
+ menuWindow.innerHTML =
+ '' +
+ '
Add Account
' +
+ '
' +
+ '
' +
+ '
' +
+ '
' +
+ '
Add Account
' +
+ '
Back
' +
+ '
' +
+ '
';
+
+ // Stop Krunker's global keydown handler from eating keystrokes in our inputs
+ menuWindow.querySelectorAll('input.accountInput').forEach((input) => {
+ input.addEventListener('keydown', (e) => e.stopPropagation());
+ });
+
+ document.getElementById('kpcAltBackBtn')!.addEventListener('click', renderAccountList);
+ document.getElementById('kpcAltSaveBtn')!.addEventListener('click', () => {
+ const label = (document.getElementById('kpcAltLabel') as HTMLInputElement).value.trim();
+ const user = (document.getElementById('kpcAltUser') as HTMLInputElement).value.trim();
+ const pass = (document.getElementById('kpcAltPass') as HTMLInputElement).value;
+ if (!label || !user || !pass) return;
+ ipcRenderer.invoke('alt-save', {
+ label,
+ username: user,
+ password: pass,
+ }).then(() => renderAccountList());
+ });
+ }
+
+ renderAccountList();
+ }
+
+ altBtn.addEventListener('click', (e) => {
+ e.stopPropagation();
+ (window as any).playSelect?.();
+ showAltManager();
+ });
+
+ function injectAltBtn(): boolean {
+ if (document.getElementById('kpcAltBtn')) return true;
+ const menuContainer = document.getElementById('menuItemContainer');
+ if (!menuContainer) return false;
+ const exitBtn = document.getElementById('clientExit');
+ if (exitBtn) {
+ menuContainer.insertBefore(altBtn, exitBtn);
+ } else {
+ menuContainer.appendChild(altBtn);
+ }
+ return true;
+ }
+
+ if (!injectAltBtn()) {
+ let attempts = 0;
+ const poll = setInterval(() => {
+ if (injectAltBtn() || ++attempts > 60) clearInterval(poll);
+ }, 500);
+ }
+ });
+ }
+
+ }).catch(() => {});
+
+ const pollInterval = setInterval(() => {
+ const w = window as any;
+ if (
+ Object.hasOwn(w, 'showWindow')
+ && typeof w.showWindow === 'function'
+ && Object.hasOwn(w, 'windows')
+ && Array.isArray(w.windows)
+ && w.windows.length >= 0
+ && typeof w.windows[0] !== 'undefined'
+ && typeof w.windows[0].changeTab === 'function'
+ ) {
+ clearInterval(pollInterval);
+ _console.log('[KCC] Settings window found, hooking...');
+ hookSettings();
+ }
+ }, 500);
+});
+
+// ── Lightweight tab page init (skips game-only features) ──
+ipcRenderer.on('main_did-finish-load-tab', () => {
+ _console.log('[KCC] Tab page loaded');
+ (window as any).closeClient = () => window.close();
+});