import { ipcRenderer } from 'electron'; import { fetchGame, MATCHMAKER_GAMEMODES, MATCHMAKER_REGIONS, MATCHMAKER_REGION_NAMES } from './matchmaker'; import type { MatchmakerConfig } from './matchmaker'; import { initUserscripts, getInstances, setScriptEnabled } from './userscripts'; import type { UserscriptInstance, UserscriptSetting } from './userscripts'; import { initTranslator, updateTranslatorConfig } from './translator'; import { setDeathAnimBlock, escapeHtml } from './utils'; // ── 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 (_e) { /* 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 ── function hasOwn(obj: any, key: string): boolean { return Object.prototype.hasOwnProperty.call(obj, key); } // ── Keybind types + helpers ── interface Keybind { key: string; ctrl: boolean; shift: boolean; alt: boolean; } 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; } 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'); document.addEventListener('keydown', kbKeydownHandler, true); document.addEventListener('keyup', kbKeyupHandler, true); const uiBase = document.getElementById('uiBase'); if (uiBase) uiBase.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) + '' + label + '' + '' + keybindDisplayString(currentBind) + '' + '
' + 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) + '' + opts.label + '' + '' + '
' + 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) + '' + opts.label + '' + '
' + 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) + '' + opts.label + '' + '' + '
' + '' + '
' + '
' + 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 = '' + 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 = '' + 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; } 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: 'New Window' }, { value: 'Same Window', label: 'Same Window' }], 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); }, })); if (ui.deathscreenAnimation) setDeathAnimBlock(true); if (ui.hideMenuPopups) startHidePopups(); 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)); 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): void { const swapEnabled = swapperConf ? swapperConf.enabled : true; 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); } function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag): void { const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true }; function saveMM(): void { ipcRenderer.invoke('set-config', 'matchmaker', mm); } body.appendChild(createToggleRow({ label: 'Custom Matchmaker', desc: 'Press F6 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(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_GAMEMODES.map(gm => ({ value: gm, label: gm })), selected: mm.gamemodes, 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 encodeCredential(decoded: string): string { const key = decoded.length; return encodeURIComponent( decoded.split('').map(c => String.fromCharCode(c.charCodeAt(0) + key)).join('') ); } function decodeCredential(encoded: string): string { const str = decodeURIComponent(encoded); const key = str.length; return str.split('').map(c => String.fromCharCode(c.charCodeAt(0) - key)).join(''); } 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 = decodeCredential(account.username); passInput.value = decodeCredential(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; 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', () => switchToAccount(acc)); 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: encodeCredential(user), password: encodeCredential(pass), }; ipcRenderer.invoke('alt-save', newAcc).then(() => { accounts.push(newAcc); labelIn.value = ''; userIn.value = ''; passIn.value = ''; form.style.display = 'none'; renderList(); }); }); } function buildTranslatorSection(body: HTMLElement, translatorConf: any): void { 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, isWindows: boolean, ): void { const advDefaults = { removeUselessFeatures: true, gpuRasterizing: false, helpfulFlags: true, disableAccelerated2D: false, increaseLimits: false, lowLatency: false, experimentalFlags: false, angleBackend: 'default', }; const adv = { ...advDefaults, ...advConf }; 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: 'disableAccelerated2D', label: 'Disable Accelerated 2D Canvas', desc: 'Disables hardware-accelerated 2D canvas rendering', 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 accelerated 2D canvas', 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(); }, })); } } // ── 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 tlSec = createSection('Translator'); container.appendChild(tlSec.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); buildMatchmakerSection(mmSec.body, mmConf, bag); buildTranslatorSection(tlSec.body, translatorConf); buildDiscordSection(discordSec.body, discordConf); buildAccountsSection(accSec.body, allConf.accounts); buildAdvancedSection(advSec.body, advConf, 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 = inst.meta.name || inst.filename; let metaParts: string[] = []; if (inst.meta.author) metaParts.push('by ' + inst.meta.author); if (inst.meta.version) metaParts.push('v' + inst.meta.version); const metaLine = metaParts.length > 0 ? '' + metaParts.join(' · ') + '' : ''; const descText = 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 [key, 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 = '' + setting.title + '' + (setting.desc ? '
' + 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, key); }); 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, key); }); 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, key); }); 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, key); }); 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, key); }); }); row.appendChild(keyEl); break; } } container.appendChild(row); } } function saveScriptSetting(inst: UserscriptInstance, _key: string): 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']), ipcRenderer.invoke('get-platform'), ]).then(([allConf, platformInfo]: [any, any]) => { const uiConf = allConf.ui; const usConf = allConf.userscripts; const gameConf = allConf.game; const translatorConf = allConf.translator; const discordConf = allConf.discord; // ── 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(); // ── 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 = ''; 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 {} } 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((accounts: any[]) => { if (!accounts || accounts.length === 0) return; 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) + '' + '' + '
' + '
' + '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'; switchToAccount(accs[idx]); } }); }); 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
' + '
' + '
'; 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: encodeCredential(user), password: encodeCredential(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 ( hasOwn(w, 'showWindow') && typeof w.showWindow === 'function' && 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); });