Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 088db29377 | |||
| d9512a9040 | |||
| 5a18242f72 | |||
| ae359497be | |||
| db89352ed8 | |||
| c1d1f6bce3 | |||
| 6582ddf93a | |||
| fba25a9081 | |||
| ca5c0491b7 | |||
| 2b2ff703c2 | |||
| 13e42e4374 | |||
| 3c7c559748 | |||
| 9e466ea9c4 | |||
| 645a93d3c4 | |||
| d311bf4a7e |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "krunker-civilian-client",
|
||||
"version": "0.7.2",
|
||||
"version": "0.7.5",
|
||||
"description": "Cross-platform Krunker game client",
|
||||
"main": "dist/main/index.js",
|
||||
"homepage": "https://github.com/bigjakk/Krunker-Civilian-Client",
|
||||
|
||||
+9
-7
@@ -33,6 +33,7 @@ export interface AppConfig {
|
||||
game: {
|
||||
lastServer: string;
|
||||
socialTabBehaviour: 'New Window' | 'Same Window';
|
||||
rememberTabs: boolean;
|
||||
joinAsSpectator: boolean;
|
||||
rawInput: boolean;
|
||||
betterChat: boolean;
|
||||
@@ -53,7 +54,7 @@ export interface AppConfig {
|
||||
maxPlayers: number;
|
||||
minRemainingTime: number;
|
||||
openServerBrowser: boolean;
|
||||
autoJoin: boolean;
|
||||
sortByPlayers: boolean;
|
||||
};
|
||||
keybinds: {
|
||||
reload: Keybind;
|
||||
@@ -62,7 +63,6 @@ export interface AppConfig {
|
||||
joinFromClipboard: Keybind;
|
||||
devTools: Keybind;
|
||||
matchmaker: Keybind;
|
||||
matchmakerAccept: Keybind;
|
||||
matchmakerCancel: Keybind;
|
||||
fullscreenToggle: Keybind;
|
||||
};
|
||||
@@ -108,6 +108,7 @@ export interface AppConfig {
|
||||
y: number | undefined;
|
||||
maximized: boolean;
|
||||
};
|
||||
savedTabs: string[];
|
||||
}
|
||||
|
||||
export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
|
||||
@@ -117,7 +118,6 @@ export const DEFAULT_KEYBINDS: AppConfig['keybinds'] = {
|
||||
joinFromClipboard: { key: 'j', ctrl: true, shift: false, alt: false },
|
||||
devTools: { key: 'F12', ctrl: false, shift: false, alt: false },
|
||||
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 },
|
||||
};
|
||||
@@ -145,6 +145,7 @@ export const config = new Store<AppConfig>({
|
||||
game: {
|
||||
lastServer: '',
|
||||
socialTabBehaviour: 'New Window',
|
||||
rememberTabs: false,
|
||||
joinAsSpectator: false,
|
||||
rawInput: true,
|
||||
betterChat: true,
|
||||
@@ -153,7 +154,7 @@ export const config = new Store<AppConfig>({
|
||||
hpEnemyCounter: true,
|
||||
},
|
||||
swapper: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
path: '',
|
||||
},
|
||||
matchmaker: {
|
||||
@@ -165,11 +166,11 @@ export const config = new Store<AppConfig>({
|
||||
maxPlayers: 6,
|
||||
minRemainingTime: 120,
|
||||
openServerBrowser: true,
|
||||
autoJoin: false,
|
||||
sortByPlayers: false,
|
||||
},
|
||||
keybinds: DEFAULT_KEYBINDS,
|
||||
userscripts: {
|
||||
enabled: true,
|
||||
enabled: false,
|
||||
path: '',
|
||||
},
|
||||
ui: {
|
||||
@@ -185,7 +186,7 @@ export const config = new Store<AppConfig>({
|
||||
lastSeenVersion: '',
|
||||
},
|
||||
discord: {
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
},
|
||||
translator: {
|
||||
enabled: true,
|
||||
@@ -210,5 +211,6 @@ export const config = new Store<AppConfig>({
|
||||
y: undefined,
|
||||
maximized: true,
|
||||
},
|
||||
savedTabs: [],
|
||||
},
|
||||
});
|
||||
|
||||
+5
-3
@@ -307,7 +307,7 @@ async function launchApp(): Promise<void> {
|
||||
session: ses,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
sandbox: true,
|
||||
spellcheck: false,
|
||||
backgroundThrottling: false,
|
||||
},
|
||||
@@ -389,7 +389,6 @@ async function launchApp(): Promise<void> {
|
||||
if (mm.enabled) {
|
||||
win.webContents.send('matchmaker-find', {
|
||||
...mm,
|
||||
acceptKey: binds.matchmakerAccept,
|
||||
cancelKey: binds.matchmakerCancel,
|
||||
});
|
||||
} else {
|
||||
@@ -411,7 +410,6 @@ async function launchApp(): Promise<void> {
|
||||
if (mm.enabled) {
|
||||
win.webContents.send('matchmaker-find', {
|
||||
...mm,
|
||||
acceptKey: binds.matchmakerAccept,
|
||||
cancelKey: binds.matchmakerCancel,
|
||||
});
|
||||
} else {
|
||||
@@ -459,10 +457,14 @@ async function launchApp(): Promise<void> {
|
||||
// ── Tab Manager ──
|
||||
const preloadPath = join(__dirname, '..', 'preload', 'index.js');
|
||||
let tabMode: 'same' | 'new' = getGameConf().socialTabBehaviour === 'Same Window' ? 'same' : 'new';
|
||||
let sessionTabs: string[] = [];
|
||||
let tabManager = new TabManager(
|
||||
win, ses, preloadPath, tabMode, isGameURL,
|
||||
() => config.get('tabWindow'),
|
||||
(state) => config.set('tabWindow', state),
|
||||
() => sessionTabs,
|
||||
(urls) => { sessionTabs = urls; },
|
||||
() => config.get('game.rememberTabs') ?? false,
|
||||
);
|
||||
|
||||
// Intercept in-page navigation (e.g. window.location = '/social.html')
|
||||
|
||||
+40
-2
@@ -42,7 +42,11 @@ export class TabManager {
|
||||
private recentlyClosed: { url: string; title: string }[] = [];
|
||||
private getTabWindowState: () => TabWindowState;
|
||||
private saveTabWindowState: (state: TabWindowState) => void;
|
||||
private getSavedTabs: () => string[];
|
||||
private saveTabs: (urls: string[]) => void;
|
||||
private isRememberEnabled: () => boolean;
|
||||
private tabSaveTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private restoredTabs = false;
|
||||
|
||||
constructor(
|
||||
win: BrowserWindow,
|
||||
@@ -52,6 +56,9 @@ export class TabManager {
|
||||
isGameURL: (url: string) => boolean,
|
||||
getTabWindowState: () => TabWindowState,
|
||||
saveTabWindowState: (state: TabWindowState) => void,
|
||||
getSavedTabs: () => string[],
|
||||
saveTabs: (urls: string[]) => void,
|
||||
isRememberEnabled: () => boolean,
|
||||
) {
|
||||
this.mainWin = win;
|
||||
this.ses = ses;
|
||||
@@ -60,6 +67,9 @@ export class TabManager {
|
||||
this.isGameURL = isGameURL;
|
||||
this.getTabWindowState = getTabWindowState;
|
||||
this.saveTabWindowState = saveTabWindowState;
|
||||
this.getSavedTabs = getSavedTabs;
|
||||
this.saveTabs = saveTabs;
|
||||
this.isRememberEnabled = isRememberEnabled;
|
||||
|
||||
// ── Tab bar view (shared between both modes) ──
|
||||
this.tabBarView = new WebContentsView({
|
||||
@@ -185,8 +195,30 @@ export class TabManager {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Open a new tab ──
|
||||
// ── Restore saved tabs on first open, then open the requested tab ──
|
||||
openTab(url: string): number {
|
||||
if (!this.restoredTabs) {
|
||||
this.restoredTabs = true;
|
||||
const saved = this.isRememberEnabled() ? this.getSavedTabs() : [];
|
||||
this.saveTabs([]);
|
||||
if (saved.length > 0) {
|
||||
for (const savedUrl of saved) {
|
||||
this.openSingleTab(savedUrl);
|
||||
}
|
||||
// If the requested URL is already among the restored tabs, just activate it
|
||||
const existing = this.tabs.find(t => t.url === url);
|
||||
if (existing) {
|
||||
this.switchToTab(existing.id);
|
||||
this.showTabs();
|
||||
return existing.id;
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.openSingleTab(url);
|
||||
}
|
||||
|
||||
// ── Open a single new tab ──
|
||||
private openSingleTab(url: string): number {
|
||||
if (this.tabs.length >= MAX_TABS) {
|
||||
const existing = this.tabs.find(t => t.url === url);
|
||||
if (existing) {
|
||||
@@ -221,7 +253,7 @@ export class TabManager {
|
||||
session: this.ses,
|
||||
contextIsolation: false,
|
||||
nodeIntegration: false,
|
||||
sandbox: false,
|
||||
sandbox: true,
|
||||
spellcheck: false,
|
||||
},
|
||||
});
|
||||
@@ -489,6 +521,12 @@ export class TabManager {
|
||||
}
|
||||
|
||||
private destroyAllTabs(): void {
|
||||
// Persist tab URLs so they can be restored later
|
||||
if (this.tabs.length > 0 && this.isRememberEnabled()) {
|
||||
this.saveTabs(this.tabs.map(t => t.url));
|
||||
this.restoredTabs = false;
|
||||
}
|
||||
|
||||
for (const tab of this.tabs) {
|
||||
this.stopTitleWatcher(tab.id);
|
||||
if (this.activeTabId === tab.id) {
|
||||
|
||||
+175
-121
@@ -321,20 +321,23 @@ function createNumberRow(opts: {
|
||||
max: number;
|
||||
value: number;
|
||||
onChange: (value: number) => void;
|
||||
step?: number;
|
||||
safety?: number;
|
||||
restart?: boolean;
|
||||
instant?: boolean;
|
||||
refreshOnly?: boolean;
|
||||
}): HTMLElement {
|
||||
const s = opts.safety || 0;
|
||||
const step = opts.step || 1;
|
||||
const parse = step < 1 ? parseFloat : parseInt;
|
||||
const row = document.createElement('div');
|
||||
row.className = 'setting settName safety-' + s + ' num';
|
||||
row.innerHTML =
|
||||
settingIcon(s, opts.instant, opts.refreshOnly, opts.restart) +
|
||||
'<span class="setting-title">' + escapeHtml(opts.label) + '</span>' +
|
||||
'<span class="setting-input-wrapper">' +
|
||||
'<div class="slidecontainer"><input type="range" class="sliderM s-update-secondary" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '"></div>' +
|
||||
'<input type="number" class="rb-input s-update sliderVal" min="' + opts.min + '" max="' + opts.max + '" value="' + opts.value + '">' +
|
||||
'<div class="slidecontainer"><input type="range" class="sliderM s-update-secondary" min="' + opts.min + '" max="' + opts.max + '" step="' + step + '" value="' + opts.value + '"></div>' +
|
||||
'<input type="number" class="rb-input s-update sliderVal" min="' + opts.min + '" max="' + opts.max + '" step="' + step + '" value="' + opts.value + '">' +
|
||||
'</span>' +
|
||||
'<div class="setting-desc-new">' + escapeHtml(opts.desc) + '</div>';
|
||||
const rangeInput = row.querySelector('input[type="range"]') as HTMLInputElement;
|
||||
@@ -343,7 +346,7 @@ function createNumberRow(opts: {
|
||||
numInput.value = rangeInput.value;
|
||||
});
|
||||
rangeInput.addEventListener('change', () => {
|
||||
const v = Math.max(opts.min, Math.min(opts.max, parseInt(rangeInput.value) || 0));
|
||||
const v = Math.max(opts.min, Math.min(opts.max, parse(rangeInput.value) || 0));
|
||||
rangeInput.value = String(v);
|
||||
numInput.value = String(v);
|
||||
opts.onChange(v);
|
||||
@@ -351,7 +354,7 @@ function createNumberRow(opts: {
|
||||
else if (opts.refreshOnly) onSettingChanged('refresh');
|
||||
});
|
||||
numInput.addEventListener('change', () => {
|
||||
const v = Math.max(opts.min, Math.min(opts.max, parseInt(numInput.value) || 0));
|
||||
const v = Math.max(opts.min, Math.min(opts.max, parse(numInput.value) || 0));
|
||||
numInput.value = String(v);
|
||||
rangeInput.value = String(v);
|
||||
opts.onChange(v);
|
||||
@@ -543,19 +546,9 @@ interface SettingsBag {
|
||||
}
|
||||
|
||||
function buildGeneralSection(
|
||||
body: HTMLElement, gameConf: any, uiConfRaw: any, perfConf: any, bag: SettingsBag,
|
||||
body: HTMLElement, gameConf: any, uiConfRaw: 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 gameDefaults = { lastServer: '', socialTabBehaviour: 'New Window', rememberTabs: false };
|
||||
const game = { ...gameDefaults, ...gameConf };
|
||||
|
||||
body.appendChild(createSelectRow({
|
||||
@@ -566,6 +559,13 @@ function buildGeneralSection(
|
||||
onChange: (v) => { game.socialTabBehaviour = v; ipcRenderer.invoke('set-config', 'game', game); },
|
||||
}));
|
||||
|
||||
body.appendChild(createToggleRow({
|
||||
label: 'Remember Tabs',
|
||||
desc: 'Restore your open tabs when you reopen the social/hub window',
|
||||
checked: game.rememberTabs, instant: true,
|
||||
onChange: (v) => { game.rememberTabs = v; ipcRenderer.invoke('set-config', 'game', game); },
|
||||
}));
|
||||
|
||||
const uiDefaults = { showExitButton: true, deathscreenAnimation: false, hideMenuPopups: false };
|
||||
const ui = { ...uiDefaults, ...uiConfRaw };
|
||||
|
||||
@@ -584,6 +584,72 @@ function buildGeneralSection(
|
||||
},
|
||||
}));
|
||||
|
||||
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: 'Show Changelog',
|
||||
desc: 'Show release notes popup when the client updates',
|
||||
checked: ui.showChangelog ?? true, instant: true,
|
||||
onChange: (v) => { ui.showChangelog = v; saveUI(); },
|
||||
}));
|
||||
|
||||
body.appendChild(createKeybindRow('Toggle Fullscreen', 'Fullscreen the game window (default F11)', bag.binds.fullscreenToggle, (b) => {
|
||||
bag.binds.fullscreenToggle = b;
|
||||
bag.saveBinds();
|
||||
}, undefined, true));
|
||||
}
|
||||
|
||||
function buildGameSection(
|
||||
body: HTMLElement, gameConf: any, uiConfRaw: any, bag: SettingsBag,
|
||||
): void {
|
||||
const game = { rawInput: true, showPing: true, hpEnemyCounter: true, ...gameConf };
|
||||
const ui = { deathscreenAnimation: false, hideMenuPopups: false, menuTimer: true, doublePing: true, ...uiConfRaw };
|
||||
|
||||
function saveGame(): void {
|
||||
ipcRenderer.invoke('set-config', 'game', game);
|
||||
}
|
||||
function saveUI(): void {
|
||||
ipcRenderer.invoke('set-config', 'ui', ui);
|
||||
}
|
||||
|
||||
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; saveGame(); },
|
||||
}));
|
||||
}
|
||||
|
||||
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; saveGame(); },
|
||||
}));
|
||||
|
||||
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: 'Hardpoint Enemy Counter',
|
||||
desc: 'Show enemy capture points in Hardpoint mode',
|
||||
checked: game.hpEnemyCounter ?? true, refreshOnly: true,
|
||||
onChange: (v) => {
|
||||
game.hpEnemyCounter = v; saveGame();
|
||||
if (v) initHPCounter(); else destroyHPCounter();
|
||||
},
|
||||
}));
|
||||
|
||||
body.appendChild(createToggleRow({
|
||||
label: 'Block Death Screen Animation',
|
||||
desc: 'Disable the slide-in animation on the death screen',
|
||||
@@ -601,13 +667,6 @@ function buildGeneralSection(
|
||||
},
|
||||
}));
|
||||
|
||||
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',
|
||||
@@ -615,63 +674,58 @@ function buildGeneralSection(
|
||||
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 {
|
||||
function buildPerformanceSection(
|
||||
body: HTMLElement, perfConf: any, isWindows: boolean,
|
||||
): void {
|
||||
const perf = { fpsUnlocked: true, cpuThrottleGame: 1, cpuThrottleMenu: 1.5, processPriority: 'Normal', ...perfConf };
|
||||
|
||||
function savePerf(): void {
|
||||
ipcRenderer.invoke('set-config', 'performance', perf);
|
||||
}
|
||||
|
||||
body.appendChild(createToggleRow({
|
||||
label: 'Unlimited FPS',
|
||||
desc: 'Uncap the frame rate (requires restart)',
|
||||
checked: perf.fpsUnlocked, restart: true,
|
||||
onChange: (v) => { perf.fpsUnlocked = v; savePerf(); },
|
||||
}));
|
||||
|
||||
body.appendChild(createNumberRow({
|
||||
label: 'CPU Throttle (Game)', desc: 'CPU throttle rate during gameplay (1 = no throttle, 3 = heavy throttle)',
|
||||
min: 1, max: 3, step: 0.01, 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, step: 0.01, 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(); },
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
function buildSwapperSection(body: HTMLElement, swapperConf: 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',
|
||||
@@ -697,6 +751,14 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any
|
||||
swapFolderBtn.addEventListener('click', () => ipcRenderer.invoke('open-swap-folder'));
|
||||
folderRow.appendChild(swapFolderBtn);
|
||||
body.appendChild(folderRow);
|
||||
}
|
||||
|
||||
function buildAppearanceSection(body: HTMLElement, uiConfRaw: any): void {
|
||||
const ui = { cssTheme: 'disabled', loadingTheme: 'disabled', backgroundUrl: '', ...uiConfRaw };
|
||||
|
||||
function saveUI(): void {
|
||||
ipcRenderer.invoke('set-config', 'ui', ui);
|
||||
}
|
||||
|
||||
// ── CSS Theme selector (populated from swap/themes/) ──
|
||||
const themeRow = document.createElement('div');
|
||||
@@ -767,10 +829,31 @@ function buildSwapperSection(body: HTMLElement, swapperConf: any, uiConfRaw: any
|
||||
saveUI();
|
||||
onSettingChanged('refresh');
|
||||
});
|
||||
|
||||
// ── Background URL (overrides loading theme selection) ──
|
||||
const urlRow = document.createElement('div');
|
||||
urlRow.className = 'setting settName safety-0';
|
||||
urlRow.innerHTML =
|
||||
refreshIcon('refresh-icon') +
|
||||
'<span class="setting-title">Background URL</span>' +
|
||||
'<div class="setting-desc-new">Direct image URL for loading screen (overrides dropdown above)</div>';
|
||||
const urlInput = document.createElement('input');
|
||||
urlInput.type = 'text';
|
||||
urlInput.className = 'inputGrey2';
|
||||
urlInput.placeholder = 'https://example.com/image.png';
|
||||
urlInput.value = ui.backgroundUrl || '';
|
||||
urlInput.style.width = '300px';
|
||||
urlInput.addEventListener('change', () => {
|
||||
ui.backgroundUrl = urlInput.value.trim();
|
||||
saveUI();
|
||||
onSettingChanged('refresh');
|
||||
});
|
||||
urlRow.appendChild(urlInput);
|
||||
body.appendChild(urlRow);
|
||||
}
|
||||
|
||||
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 };
|
||||
const mm = mmConf || { enabled: true, regions: [], gamemodes: [], minPlayers: 1, maxPlayers: 6, minRemainingTime: 120, openServerBrowser: true, sortByPlayers: false };
|
||||
|
||||
function saveMM(): void {
|
||||
ipcRenderer.invoke('set-config', 'matchmaker', mm);
|
||||
@@ -791,20 +874,16 @@ function buildMatchmakerSection(body: HTMLElement, mmConf: any, bag: SettingsBag
|
||||
}));
|
||||
|
||||
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(); },
|
||||
label: 'Prioritize Player Count',
|
||||
desc: 'Sort results by most players first, then by ping (default is ping first)',
|
||||
checked: mm.sortByPlayers ?? false, instant: true,
|
||||
onChange: (v) => { mm.sortByPlayers = 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();
|
||||
@@ -1071,7 +1150,7 @@ function buildChatSection(body: HTMLElement, gameConf: any, translatorConf: any)
|
||||
}
|
||||
|
||||
function buildAdvancedSection(
|
||||
body: HTMLElement, advConf: any, perfConf: any, isWindows: boolean,
|
||||
body: HTMLElement, advConf: any, isWindows: boolean,
|
||||
): void {
|
||||
const advDefaults = {
|
||||
removeUselessFeatures: true,
|
||||
@@ -1084,11 +1163,6 @@ function buildAdvancedSection(
|
||||
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);
|
||||
@@ -1135,34 +1209,6 @@ function buildAdvancedSection(
|
||||
}));
|
||||
}
|
||||
|
||||
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',
|
||||
@@ -1270,8 +1316,14 @@ function renderSettings(searchQuery?: string): void {
|
||||
// ── Create section shells ──
|
||||
const genSec = createSection('General');
|
||||
container.appendChild(genSec.section);
|
||||
const gameSec = createSection('Game');
|
||||
container.appendChild(gameSec.section);
|
||||
const perfSec = createSection('Performance');
|
||||
container.appendChild(perfSec.section);
|
||||
const swapSec = createSection('Swapper');
|
||||
container.appendChild(swapSec.section);
|
||||
const appearSec = createSection('Appearance');
|
||||
container.appendChild(appearSec.section);
|
||||
const mmSec = createSection('Matchmaker');
|
||||
container.appendChild(mmSec.section);
|
||||
const chatSec = createSection('Chat');
|
||||
@@ -1300,7 +1352,6 @@ function renderSettings(searchQuery?: string): void {
|
||||
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 },
|
||||
};
|
||||
@@ -1314,13 +1365,16 @@ function renderSettings(searchQuery?: string): void {
|
||||
};
|
||||
|
||||
// Populate each section
|
||||
buildGeneralSection(genSec.body, gameConf, uiConfRaw, allConf.performance, bag);
|
||||
buildSwapperSection(swapSec.body, swapperConf, uiConfRaw);
|
||||
buildGeneralSection(genSec.body, gameConf, uiConfRaw, bag);
|
||||
buildGameSection(gameSec.body, gameConf, uiConfRaw, bag);
|
||||
buildPerformanceSection(perfSec.body, allConf.performance, isWindows);
|
||||
buildSwapperSection(swapSec.body, swapperConf);
|
||||
buildAppearanceSection(appearSec.body, 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);
|
||||
buildAdvancedSection(advSec.body, advConf, isWindows);
|
||||
renderUserscriptsSection(usSec.body);
|
||||
|
||||
if (searchQuery) applySearchFilter(container, holder, searchQuery);
|
||||
|
||||
+17
-96
@@ -17,8 +17,8 @@ export const MATCHMAKER_GAMEMODE_FILTER = [
|
||||
'Domination', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers',
|
||||
'Bighead FFA',
|
||||
];
|
||||
export const MATCHMAKER_REGIONS = ['MBI', 'NY', 'FRA', 'SIN', 'DAL', 'SYD', 'MIA', 'BHN', 'TOK', 'BRZ', 'AFR', 'LON', 'CHI', 'SV', 'STL', 'MX'];
|
||||
export const MATCHMAKER_REGION_NAMES: Record<string, string> = { MBI: 'Mumbai', NY: 'New York', FRA: 'Frankfurt', SIN: 'Singapore', DAL: 'Dallas', SYD: 'Sydney', MIA: 'Miami', BHN: 'Middle East', TOK: 'Tokyo', BRZ: 'Brazil', AFR: 'South Africa', LON: 'London', CHI: 'China', SV: 'Silicon Valley', STL: 'Seattle', MX: 'Mexico' };
|
||||
export const MATCHMAKER_REGIONS = ['SV', 'TOK', 'FRA', 'MBI', 'SYD', 'SIN', 'DAL', 'BHN', 'BRZ', 'NY'];
|
||||
export const MATCHMAKER_REGION_NAMES: Record<string, string> = { SV: 'Silicon Valley', TOK: 'Tokyo', FRA: 'Frankfurt', MBI: 'Mumbai', SYD: 'Sydney', SIN: 'Singapore', DAL: 'Dallas', BHN: 'Bahrain', BRZ: 'Brazil', NY: 'New York' };
|
||||
export const MAP_ICON_INDICES = ['Burg', 'Littletown', 'Sandstorm', 'Subzero', 'Undergrowth', 'Shipment', 'Freight', 'Lostworld', 'Citadel', 'Oasis', 'Kanji', 'Industry', 'Lumber', 'Evacuation', 'Site', 'SkyTemple', 'Lagoon', 'Bureau', 'Tortuga', 'Tropicano', 'Krunk_Plaza', 'Arena', 'Habitat', 'Atomic', 'Old_Burg', 'Throwback', 'Stockade', 'Facility', 'Clockwork', 'Laboratory', 'Shipyard', 'Soul Sanctum', 'Bazaar', 'Erupt', 'HQ', 'Khepri', 'Lush', 'Vivo', 'Slide Moonlight', 'Eterno Sim'];
|
||||
export const MATCHMAKER_MAP_NAMES: Record<string, string> = {
|
||||
SkyTemple: 'Sky Temple', Krunk_Plaza: 'Krunk Plaza', Old_Burg: 'Old Burg',
|
||||
@@ -65,18 +65,10 @@ export interface MatchmakerConfig {
|
||||
maxPlayers: number;
|
||||
minRemainingTime: number;
|
||||
openServerBrowser: boolean;
|
||||
autoJoin: boolean;
|
||||
acceptKey: Keybind;
|
||||
sortByPlayers: boolean;
|
||||
cancelKey: Keybind;
|
||||
}
|
||||
|
||||
function secondsToTimestring(num: number): string {
|
||||
const minutes = Math.floor(num / 60);
|
||||
const seconds = num % 60;
|
||||
if (minutes < 1) return `${num}s`;
|
||||
return `${minutes}m ${seconds}s`;
|
||||
}
|
||||
|
||||
function matchesKey(bind: Keybind, event: KeyboardEvent): boolean {
|
||||
if ((document.activeElement as HTMLElement)?.tagName === 'INPUT') return false;
|
||||
return event.key === bind.key
|
||||
@@ -102,21 +94,17 @@ popupElement.appendChild(popupDescription);
|
||||
const popupOptions = document.createElement('div');
|
||||
popupOptions.id = 'matchmakerPopupOptions';
|
||||
|
||||
const popupConfirmBtn = document.createElement('div');
|
||||
popupConfirmBtn.id = 'matchmakerConfirmButton';
|
||||
popupConfirmBtn.className = 'matchmakerPopupButton bigShadowT';
|
||||
popupConfirmBtn.textContent = 'Join';
|
||||
popupConfirmBtn.setAttribute('onmouseenter', 'playTick()');
|
||||
popupConfirmBtn.addEventListener('click', () => decideMatchmakerDecision(true));
|
||||
|
||||
const popupCancelBtn = document.createElement('div');
|
||||
popupCancelBtn.id = 'matchmakerCancelButton';
|
||||
popupCancelBtn.className = 'matchmakerPopupButton bigShadowT';
|
||||
popupCancelBtn.textContent = 'Cancel';
|
||||
popupCancelBtn.setAttribute('onmouseenter', 'playTick()');
|
||||
popupCancelBtn.addEventListener('click', () => decideMatchmakerDecision(false));
|
||||
popupCancelBtn.addEventListener('click', () => {
|
||||
const w = window as any;
|
||||
if (typeof w.playSelect === 'function') w.playSelect();
|
||||
dismissPopup();
|
||||
});
|
||||
|
||||
popupOptions.appendChild(popupConfirmBtn);
|
||||
popupOptions.appendChild(popupCancelBtn);
|
||||
popupElement.appendChild(popupOptions);
|
||||
|
||||
@@ -146,10 +134,8 @@ searchContainer.appendChild(searchCancelBtn);
|
||||
popupElement.appendChild(searchContainer);
|
||||
|
||||
// ── State ──
|
||||
let popupGameID = '';
|
||||
let popupCandidates: MatchmakerGame[] = [];
|
||||
let openServerBrowser = true;
|
||||
let confirmKey: Keybind = { key: 'Enter', ctrl: false, shift: false, alt: false };
|
||||
let cancelKey: Keybind = { key: 'Escape', ctrl: false, shift: false, alt: false };
|
||||
let searchAborted = false;
|
||||
|
||||
@@ -191,25 +177,11 @@ async function verifyAndJoin(gameID: string): Promise<void> {
|
||||
|
||||
function dismissPopup(): void {
|
||||
document.removeEventListener('keydown', handleSearchBind, true);
|
||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
||||
|
||||
if (popupElement.parentNode) popupElement.remove();
|
||||
popupElement.classList.remove('searching');
|
||||
}
|
||||
|
||||
function decideMatchmakerDecision(accept: boolean): void {
|
||||
const w = window as any;
|
||||
if (typeof w.playSelect === 'function') w.playSelect();
|
||||
|
||||
if (accept && popupGameID !== 'none') {
|
||||
verifyAndJoin(popupGameID);
|
||||
} else {
|
||||
dismissPopup();
|
||||
if (popupGameID === 'none' && openServerBrowser && typeof w.openServerWindow === 'function') {
|
||||
w.openServerWindow(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearchBind(event: KeyboardEvent): void {
|
||||
if (document.pointerLockElement) return;
|
||||
if (matchesKey(cancelKey, event)) {
|
||||
@@ -219,42 +191,6 @@ function handleSearchBind(event: KeyboardEvent): void {
|
||||
}
|
||||
}
|
||||
|
||||
function handleMatchmakerBind(event: KeyboardEvent): void {
|
||||
if (document.pointerLockElement) return;
|
||||
const isAccept = matchesKey(confirmKey, event);
|
||||
const isCancel = matchesKey(cancelKey, event);
|
||||
if (isAccept || isCancel) {
|
||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
||||
decideMatchmakerDecision(isAccept);
|
||||
}
|
||||
}
|
||||
|
||||
function showResultPopup(game: MatchmakerGame): void {
|
||||
popupElement.classList.remove('searching');
|
||||
const mapIdx = MAP_ICON_INDICES.indexOf(game.map);
|
||||
popupElement.style.backgroundImage = `url(https://assets.krunker.io/img/maps/map_${mapIdx >= 0 ? mapIdx : 0}.png)`;
|
||||
|
||||
popupGameID = game.gameID;
|
||||
if (game.gameID === 'none') {
|
||||
popupTitle.innerText = 'No Games Found...';
|
||||
popupDescription.innerHTML = 'Check the server browser to see other lobbies.';
|
||||
popupConfirmBtn.style.display = 'none';
|
||||
} else {
|
||||
popupTitle.innerText = 'Game Found!';
|
||||
const regionName = MATCHMAKER_REGION_NAMES[game.region] ?? 'Unknown Region';
|
||||
popupDescription.innerHTML = `${escapeHtml(game.gamemode)} on ${escapeHtml(game.map)} (${escapeHtml(regionName)})<br/>${game.playerCount}/${game.playerLimit} Players, ${secondsToTimestring(game.remainingTime)} Left`;
|
||||
popupConfirmBtn.style.display = 'block';
|
||||
}
|
||||
|
||||
// Re-trigger slide animation
|
||||
popupElement.style.animation = 'none';
|
||||
void popupElement.offsetWidth;
|
||||
popupElement.style.animation = '';
|
||||
|
||||
document.removeEventListener('keydown', handleSearchBind, true);
|
||||
document.addEventListener('keydown', handleMatchmakerBind, true);
|
||||
}
|
||||
|
||||
function showSearchPopup(): void {
|
||||
searchAborted = false;
|
||||
popupElement.classList.add('searching');
|
||||
@@ -263,7 +199,7 @@ function showSearchPopup(): void {
|
||||
searchFeed.innerHTML = '';
|
||||
searchCounter.textContent = '';
|
||||
|
||||
document.removeEventListener('keydown', handleMatchmakerBind, true);
|
||||
|
||||
document.addEventListener('keydown', handleSearchBind, true);
|
||||
|
||||
const uiBase = document.getElementById('uiBase');
|
||||
@@ -358,8 +294,12 @@ async function fetchAllGames(mmConfig: MatchmakerConfig): Promise<{ all: RawLobb
|
||||
return { all, filtered };
|
||||
}
|
||||
|
||||
function sortByPingThenPlayers(games: MatchmakerGame[], pings: Record<string, number>): MatchmakerGame[] {
|
||||
function sortGames(games: MatchmakerGame[], pings: Record<string, number>, sortByPlayers: boolean): MatchmakerGame[] {
|
||||
return games.sort((a, b) => {
|
||||
if (sortByPlayers) {
|
||||
if (a.playerCount !== b.playerCount) return b.playerCount - a.playerCount;
|
||||
return (pings[a.region] ?? 999) - (pings[b.region] ?? 999);
|
||||
}
|
||||
const pingA = pings[a.region] ?? 999;
|
||||
const pingB = pings[b.region] ?? 999;
|
||||
if (pingA !== pingB) return pingA - pingB;
|
||||
@@ -369,7 +309,6 @@ function sortByPingThenPlayers(games: MatchmakerGame[], pings: Record<string, nu
|
||||
|
||||
export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole): Promise<void> {
|
||||
openServerBrowser = mmConfig.openServerBrowser;
|
||||
confirmKey = mmConfig.acceptKey;
|
||||
cancelKey = mmConfig.cancelKey;
|
||||
|
||||
// Dismiss existing popup if active (also aborts in-flight search)
|
||||
@@ -406,7 +345,7 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
||||
_con?.log('[KCC-MM]', filtered.length, '/', allLobbies.length, 'games passed filters');
|
||||
|
||||
// Sort immediately — result is ready
|
||||
if (filtered.length > 0) sortByPingThenPlayers(filtered, pings);
|
||||
if (filtered.length > 0) sortGames(filtered, pings, mmConfig.sortByPlayers);
|
||||
popupCandidates = filtered;
|
||||
|
||||
// Fire animation in background (non-blocking eye candy)
|
||||
@@ -429,8 +368,7 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
||||
const best = pool[Math.floor(Math.random() * pool.length)];
|
||||
_con?.log('[KCC-MM] Best match:', best.gameID, best.region, best.map, `(${pings[best.region] ?? '?'}ms, pool: ${pool.length})`);
|
||||
|
||||
if (mmConfig.autoJoin) {
|
||||
// Brief "Lobby Found!" flash before joining
|
||||
// Brief "Lobby Found!" flash before auto-joining
|
||||
const regionName = MATCHMAKER_REGION_NAMES[best.region] ?? best.region;
|
||||
searchStatus.textContent = 'Lobby Found!';
|
||||
searchFeed.innerHTML = '';
|
||||
@@ -445,29 +383,12 @@ export async function fetchGame(mmConfig: MatchmakerConfig, _con?: SavedConsole)
|
||||
searchCounter.textContent = `${best.gamemode} \u00B7 ${regionName} \u00B7 ${pings[best.region] ?? '?'}ms`;
|
||||
await new Promise(r => setTimeout(r, 1200));
|
||||
await verifyAndJoin(best.gameID);
|
||||
return;
|
||||
}
|
||||
|
||||
showResultPopup(best);
|
||||
} else {
|
||||
_con?.log('[KCC-MM] No matching games found');
|
||||
|
||||
if (mmConfig.autoJoin) {
|
||||
dismissPopup();
|
||||
if (openServerBrowser && typeof (window as any).openServerWindow === 'function') {
|
||||
(window as any).openServerWindow(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
showResultPopup({
|
||||
gameID: 'none',
|
||||
region: 'none',
|
||||
playerCount: 0,
|
||||
playerLimit: 0,
|
||||
map: MAP_ICON_INDICES[0],
|
||||
gamemode: MATCHMAKER_GAMEMODES[0],
|
||||
remainingTime: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ const FALSE_POSITIVE_LANGS = new Set([
|
||||
|
||||
// ── Auto-suppression (repeated short phrases) ──
|
||||
|
||||
const suppressionCounts = new Map<string, number>();
|
||||
let suppressionCounts = new Map<string, number>();
|
||||
const SUPPRESS_THRESHOLD = 3;
|
||||
const MIN_LATIN_WORDS = 3;
|
||||
const SHORT_TEXT_THRESHOLD = 15;
|
||||
@@ -113,6 +113,7 @@ const SHORT_TEXT_THRESHOLD = 15;
|
||||
|
||||
let activeRequests = 0;
|
||||
const MAX_CONCURRENT = 3;
|
||||
const MAX_QUEUE = 15;
|
||||
const pendingQueue: Array<() => void> = [];
|
||||
|
||||
function enqueue(fn: () => Promise<void>): void {
|
||||
@@ -123,10 +124,45 @@ function enqueue(fn: () => Promise<void>): void {
|
||||
if (pendingQueue.length > 0) pendingQueue.shift()!();
|
||||
});
|
||||
} else {
|
||||
// Drop oldest if queue is full — old messages have already scrolled off-screen
|
||||
if (pendingQueue.length >= MAX_QUEUE) pendingQueue.shift();
|
||||
pendingQueue.push(() => enqueue(fn));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Periodic cleanup (prevents unbounded memory growth in long sessions) ──
|
||||
|
||||
const CLEANUP_INTERVAL_MS = 5 * 60 * 1000;
|
||||
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function startCleanup(): void {
|
||||
if (cleanupTimer) return;
|
||||
cleanupTimer = setInterval(() => {
|
||||
// Reset suppression counts (re-learned naturally from fresh messages)
|
||||
suppressionCounts = new Map();
|
||||
|
||||
// Prune expired sessionStorage cache entries
|
||||
const now = Date.now();
|
||||
const keysToRemove: string[] = [];
|
||||
for (let i = 0; i < sessionStorage.length; i++) {
|
||||
const key = sessionStorage.key(i);
|
||||
if (!key?.startsWith(CACHE_KEY_PREFIX)) continue;
|
||||
try {
|
||||
const entry: CacheEntry = JSON.parse(sessionStorage.getItem(key) || '');
|
||||
if (now - entry.ts > CACHE_EXPIRY_MS) keysToRemove.push(key);
|
||||
} catch { keysToRemove.push(key); }
|
||||
}
|
||||
for (const key of keysToRemove) sessionStorage.removeItem(key);
|
||||
}, CLEANUP_INTERVAL_MS);
|
||||
}
|
||||
|
||||
function stopCleanup(): void {
|
||||
if (cleanupTimer) {
|
||||
clearInterval(cleanupTimer);
|
||||
cleanupTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── System message patterns to skip ──
|
||||
|
||||
const SYSTEM_PATTERNS = [
|
||||
@@ -281,8 +317,10 @@ function processMessage(node: HTMLElement): void {
|
||||
|
||||
const { message, username } = extracted;
|
||||
enqueue(async () => {
|
||||
// Node may have been removed by chat history trimming while queued
|
||||
if (!node.isConnected) return;
|
||||
const result = await translateText(message);
|
||||
if (result) appendTranslation(node, username, result.translation, result.srcLang);
|
||||
if (result && node.isConnected) appendTranslation(node, username, result.translation, result.srcLang);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,6 +354,7 @@ function startObserver(): void {
|
||||
});
|
||||
|
||||
chatObserver.observe(chatList, { childList: true });
|
||||
startCleanup();
|
||||
_con.log('[KCC-TL] Chat observer active');
|
||||
}, 500);
|
||||
}
|
||||
@@ -329,6 +368,8 @@ function stopObserver(): void {
|
||||
chatObserver.disconnect();
|
||||
chatObserver = null;
|
||||
}
|
||||
stopCleanup();
|
||||
pendingQueue.length = 0;
|
||||
}
|
||||
|
||||
// ── Public API ──
|
||||
|
||||
Reference in New Issue
Block a user