Skip to content

Commit

Permalink
feat: matchmaker GUI & array settings support
Browse files Browse the repository at this point in the history
we are now using krunker's classes for their css
and added eslint formatting
  • Loading branch information
KraXen72 committed Jun 12, 2023
1 parent 9fb7492 commit 9d86189
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 96 deletions.
25 changes: 14 additions & 11 deletions assets/settingCss.css
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,8 @@

.Crankshaft-settings .setting.settName .inputGrey2,
.Crankshaft-settings .setting.settName .switch,
.Crankshaft-settings .setting.settName .cs-input-num,
.Crankshaft-settings .setting.settName .crankshaft-multisel-parent {
.Crankshaft-settings .setting.settName .crankshaft-multisel-parent,
.Crankshaft-settings .setting.settName .setting-input-wrapper {
grid-area: input;
}

Expand Down Expand Up @@ -239,16 +239,19 @@ input:checked+.advancedSlider:hover {
padding: .2rem;
border-radius: 10px;
}
.crankshaft-multisel-parent label {
padding: .2rem;
border-radius: 5px;
background: #393939;
color: #a3a5aa;
position: relative;
.crankshaft-multisel-parent label.hostOpt {
width: 100%;
margin: 0;
box-sizing: border-box;
}
/*
.crankshaft-multisel-parent label input {
visibility: hidden;
}
.crankshaft-multisel-parent label input::after {
content: "";
background: transparent;
visibility: visible;
border: 2px solid rgba(128, 0, 0, 0.466);
display: block;
position: absolute;
width: 100%;
Expand All @@ -258,5 +261,5 @@ input:checked+.advancedSlider:hover {
border-radius: 5px;
}
.crankshaft-multisel-parent label input:checked::after {
background-color: rgba(0, 128, 0, 0.466);
}
border-color: rgba(0, 128, 0, 0.466);
} */
3 changes: 3 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,10 +155,13 @@ interface GameInfo {
interface IMatchmakerCriteria {
minPlayers: number,
maxPlayers: number,

/** e.g. FRA */
regions: string[],

/** e.g. 'Free for All' */
gameModes: string[],

/** remaining time in seconds */
minRemainingTime: number,
}
74 changes: 41 additions & 33 deletions src/matchmaker.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,83 @@
import { secondsToTimestring } from "./utils";
// import { strippedConsole } from "./preload";
// matchmaker code originally by wa#3991 / paintingofblue
import { secondsToTimestring } from './utils';

/*
* import { strippedConsole } from "./preload";
* matchmaker code originally by wa#3991 / paintingofblue
*/


// https://greasyfork.org/en/scripts/468482-kraxen-s-krunker-utils
export const MATCHMAKER_REGIONS = [ "MBI", "NY", "FRA", "SIN", "DAL", "SYD", "MIA", "BHN", "TOK", "BRZ", "AFR", "LON", "CHI", "SV", "STL", "MX" ]
export const MATCHMAKER_GAMEMODES = [ "Free for All", "Team Deathmatch", "Hardpoint", "Capture the Flag", "Parkour", "Hide & Seek", "Infected", "Race", "Last Man Standing", "Simon Says", "Gun Game", "Prop Hunt", "Boss Hunt", "Classic FFA", "Deposit", "Stalker", "King of the Hill", "One in the Chamber", "Trade", "Kill Confirmed", "Defuse", "Sharp Shooter", "Traitor", "Raid", "Blitz", "Domination", "Squad Deathmatch", "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'];
// eslint-disable-next-line max-len
export const MATCHMAKER_GAMEMODES = ['Free for All', 'Team Deathmatch', 'Hardpoint', 'Capture the Flag', 'Parkour', 'Hide & Seek', 'Infected', 'Race', 'Last Man Standing', 'Simon Says', 'Gun Game', 'Prop Hunt', 'Boss Hunt', 'Classic FFA', 'Deposit', 'Stalker', 'King of the Hill', 'One in the Chamber', 'Trade', 'Kill Confirmed', 'Defuse', 'Sharp Shooter', 'Traitor', 'Raid', 'Blitz', 'Domination', 'Squad Deathmatch', 'Kranked FFA', 'Team Defender', 'Deposit FFA', 'Chaos Snipers', 'Bighead FFA'];

function getGameMode(num: number) {
return MATCHMAKER_GAMEMODES[num]
return MATCHMAKER_GAMEMODES[num];
}

const matchmakerCriteria: IMatchmakerCriteria = {
regions: ['FRA'],
gameModes: ['Free for All'],
minPlayers: 4,
maxPlayers: 6,
minRemainingTime: 180,
minRemainingTime: 180
};

export function fetchGame() {
fetch(`https://matchmaker.krunker.io/game-list?hostname=${window.location.hostname}`)
.then((result) => result.json())
.then((result) => {
.then(result => result.json())
.then(result => {
const games = [];

for (const game of result.games) {
const gameID = game[0];
const region = gameID.split(':')[0];
const playerCount = game[2];
const playerLimit = game[3];
const map = game[4]['i'];
const gamemode = getGameMode(game[4]['g']);
const map = game[4].i;
const gamemode = getGameMode(game[4].g);
const remainingTime = game[5];

if (
!matchmakerCriteria.regions.includes(region) ||
!matchmakerCriteria.gameModes.includes(gamemode) ||
playerCount < matchmakerCriteria.minPlayers ||
playerCount > matchmakerCriteria.maxPlayers ||
remainingTime < matchmakerCriteria.minRemainingTime ||
playerCount === playerLimit ||
window.location.href.includes(gameID)
)
continue;
!matchmakerCriteria.regions.includes(region)
|| !matchmakerCriteria.gameModes.includes(gamemode)
|| playerCount < matchmakerCriteria.minPlayers
|| playerCount > matchmakerCriteria.maxPlayers
|| remainingTime < matchmakerCriteria.minRemainingTime
|| playerCount === playerLimit
|| window.location.href.includes(gameID)
) continue;

games.push({
gameID: gameID,
region: region,
playerCount: playerCount,
playerLimit: playerLimit,
map: map,
gamemode: gamemode,
remainingTime: remainingTime,
gameID,
region,
playerCount,
playerLimit,
map,
gamemode,
remainingTime
});
}

const game = games[Math.floor(Math.random() * games.length)];
try {
// eslint-disable-next-line max-len
const text = `Game found!\n\nRegion: ${game.region}\nMap: ${game.map}\nGamemode: ${game.gamemode}\nPlayers: ${game.playerCount}/${game.playerLimit}\nTime remaining: ${secondsToTimestring(game.remainingTime)}\n\nJoining game...`;
// eslint-disable-next-line no-alert
if (confirm(text)) window.location.href = `https://krunker.io/?game=${game.gameID}`;
} catch (e) {
// eslint-disable-next-line no-alert
alert('Unable to find an active game matching your criteria. Please adjust matchmaker criteria or try again later.');
}
});
}

// TODO mention matchmaker in settings
// TODO if main matchmaker setting is off, don't run any of this code
// TODO reimplement with menu
// TODO add a setting that overrides the f6 key in favor of normal F6
// TODO add gui config to this matchmaker
// TODO add maxPlayers setting (for example if somebody wants max 6 players to be safe & make sure to join)
/*
* TODO mention matchmaker in readme
* TODO if main matchmaker setting is off, don't run any of this code
* TODO reimplement hotkey with menu
* TODO implement the setting that overrides the f6 key in favor of normal F6
*/

// TODO possibly add map selection? although there are so many maps...

118 changes: 67 additions & 51 deletions src/settingsui.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable max-len */
import { writeFileSync } from 'fs';
import { ipcRenderer } from 'electron'; // add app if crashes
import { createElement, toggleSettingCSS } from './utils';
import { styleSettingsCSS, classPickerBottom } from './preload';
import { createElement, haveSameContents, toggleSettingCSS } from './utils';
import { styleSettingsCSS, classPickerBottom, strippedConsole } from './preload';
import { su } from './userscripts';
import { MATCHMAKER_GAMEMODES, MATCHMAKER_REGIONS } from './matchmaker';

Expand Down Expand Up @@ -70,13 +70,13 @@ const settingsDesc: SettingsDesc = {
hideReCaptcha: { title: 'Hide reCaptcha', type: 'bool', safety: 0, cat: 1, instant: true },
quickClassPicker: { title: 'Quick Class Picker', type: 'bool', safety: 0, cat: 1, instant: true },

matchmaker: { title: 'Custom Matchmaker', type: 'bool', desc: 'Configurable matchmaker. Default hotkey F1', safety: 0, cat: 2 },
matchmaker_F6: { title: 'Use F6 as Matchmaker hotkey', type: 'bool', desc: 'Replace default \'New Lobby\' F6 hotkey with Matchmaker ', safety: 0, cat: 2 },
matchmaker_regions: { title: 'Whitelisted regions', type: 'multisel', desc: '', safety: 0, cat: 2, opts: MATCHMAKER_REGIONS, cols: 8 },
matchmaker_gamemodes: { title: 'Whitelisted gamemodes', type: 'multisel', desc: '', safety: 0, cat: 2, opts: MATCHMAKER_GAMEMODES, cols: 4 },
matchmaker_minRemainingTime: { title: 'Minimum temaining time', type: 'num', min: 0, max: 3600, safety: 0, cat: 2 },
matchmaker_minPlayers: { title: 'Minimum players in Lobby', type: 'num', min: 0, max: 7, safety: 0, cat: 2 },
matchmaker_maxPlayers: { title: 'Maximum players in Lobby', type: 'num', min: 0, max: 7, safety: 0, cat: 2 },
matchmaker: { title: 'Custom Matchmaker', type: 'bool', desc: 'Configurable matchmaker. Default hotkey F1', safety: 0, cat: 2, refreshOnly: true },
matchmaker_F6: { title: 'F6 hotkey', type: 'bool', desc: 'Replace default \'New Lobby\' F6 hotkey with Matchmaker ', safety: 0, cat: 2, refreshOnly: true },
matchmaker_regions: { title: 'Whitelisted regions', type: 'multisel', desc: '', safety: 0, cat: 2, opts: MATCHMAKER_REGIONS, cols: 8, refreshOnly: true },
matchmaker_gamemodes: { title: 'Whitelisted gamemodes', type: 'multisel', desc: '', safety: 0, cat: 2, opts: MATCHMAKER_GAMEMODES, cols: 4, refreshOnly: true },
matchmaker_minRemainingTime: { title: 'Minimum remaining seconds', type: 'num', min: 0, max: 3600, safety: 0, cat: 2, refreshOnly: true },
matchmaker_minPlayers: { title: 'Minimum players in Lobby', type: 'num', min: 0, max: 7, safety: 0, cat: 2, refreshOnly: true },
matchmaker_maxPlayers: { title: 'Maximum players in Lobby', type: 'num', min: 0, max: 7, safety: 0, cat: 2, refreshOnly: true },

logDebugToConsole: { title: 'Log debug & GPU info to electron console', type: 'bool', safety: 0, cat: 3 },
alwaysWaitForDevTools: { title: 'Always wait for DevTools', desc: 'Crankshaft uses an alt. method to open Devtools in a new window if they take too long. This disables that. Might cause DevTools to not work', type: 'bool', safety: 3, cat: 3 },
Expand Down Expand Up @@ -116,12 +116,19 @@ function saveSettings() {
function recalculateRefreshNeeded() {
refreshNeeded = RefreshEnum.notNeeded;
for (let i = 0; i < Object.keys(userPrefs).length; i++) {
const cache = (item: any | any[]) => (Array.isArray(item) ? [...item] : item);
const key = Object.keys(userPrefs)[i];
const descObj = settingsDesc[key];
const setting = userPrefs[key];
const cachedSetting = userPrefsCache[key];
const setting = cache(userPrefs[key]);
const cachedSetting = cache(userPrefsCache[key]);

if (setting !== cachedSetting) {
const settingsEqual = Array.isArray(setting) && Array.isArray(cachedSetting)
? haveSameContents(setting, cachedSetting)
: setting === cachedSetting;

strippedConsole.log(settingsEqual, setting, cachedSetting);

if (!settingsEqual) {
if (descObj?.instant) {
continue;
} else if (descObj?.refreshOnly) {
Expand Down Expand Up @@ -209,7 +216,7 @@ class SettingElem {
case 'num':
this.HTML += `<span class="setting-title">${props.title}</span>
<span class="setting-input-wrapper">
<input type="number" class="rb-input s-update cs-input-num" name="${props.key}" autocomplete="off" value="${props.value}" min="${props.min}" max="${props.max}">
<input type="number" class="rb-input s-update sliderVal" name="${props.key}" autocomplete="off" value="${props.value}" min="${props.min}" max="${props.max}">
</span>`;
this.updateKey = 'value';
this.updateMethod = 'onchange';
Expand All @@ -228,9 +235,13 @@ class SettingElem {
case 'multisel':
this.HTML = `<span class="setting-title">${props.title}</span>
<div class="crankshaft-multisel-parent s-update" ${props?.cols ? `style="grid-template-columns:repeat(${props.cols}, 1fr)"` : '' }>
${props.opts.map(opt => `<label><input type="checkbox" name="${opt}"/>${opt}</label>`).join('')}
</div>`
this.updateKey = 'value';
${props.opts.map(opt => `<label class="hostOpt">
<span class="optName">${opt}</span>
<input type="checkbox" name="${opt}" ${props.value.includes(opt) ? 'checked' : ''} />
<div class="optCheck"></div>
</label>`).join('')}
</div>`;
this.updateKey = 'value'; // this is bypassed anyway, because type === 'multisel'. '' throws
this.updateMethod = 'onchange';
break;
default:
Expand All @@ -250,53 +261,60 @@ class SettingElem {
// eslint-disable-next-line @typescript-eslint/ban-types
update({ elem, callback }: { elem: Element; callback: 'normal' | 'userscript' | Function; }) {
if (this.updateKey === '') throw 'Invalid update key';
const target = elem.getElementsByClassName('s-update')[0];
const target = elem.querySelector('.s-update') as HTMLInputElement;

// @ts-ignore
const value = target[this.updateKey];
const value = this.props.type === 'multisel'
? [...target.children].filter(child => child.querySelector('input:checked')).map(child => child.querySelector('.optName').textContent)
: target[this.updateKey];

if (callback === 'normal') {
ipcRenderer.send('logMainConsole', `recieved an update for ${this.props.key}: ${value}`);
userPrefs[this.props.key] = value;
saveSettings();

// you can add custom instant refresh callbacks for settings here
if (this.props.key === 'hideAds') {
toggleSettingCSS(styleSettingsCSS.hideAds, this.props.key, value);
document.getElementById('hiddenClasses').style.bottom = value ? classPickerBottom : null;
if (typeof value === 'boolean') {
if (this.props.key === 'hideAds') {
toggleSettingCSS(styleSettingsCSS.hideAds, this.props.key, value);
document.getElementById('hiddenClasses').style.bottom = value ? classPickerBottom : null;
}
if (this.props.key === 'menuTimer') toggleSettingCSS(styleSettingsCSS.menuTimer, this.props.key, value);
if (this.props.key === 'quickClassPicker') toggleSettingCSS(styleSettingsCSS.quickClassPicker, this.props.key, value);
if (this.props.key === 'hideReCaptcha') toggleSettingCSS(styleSettingsCSS.hideReCaptcha, this.props.key, value);
}
if (this.props.key === 'menuTimer') toggleSettingCSS(styleSettingsCSS.menuTimer, this.props.key, value);
if (this.props.key === 'quickClassPicker') toggleSettingCSS(styleSettingsCSS.quickClassPicker, this.props.key, value);
if (this.props.key === 'hideReCaptcha') toggleSettingCSS(styleSettingsCSS.hideReCaptcha, this.props.key, value);
} else if (callback === 'userscript') {
let refreshSettings = false;
if ('userscriptReference' in this.props) {
const userscript = this.props.userscriptReference;

// either the userscsript has not ran yet, or it's instant
if (value && (!userscript.hasRan || this.props.instant)) {
userscript.load();
if (!userscript.hasRan) refreshSettings = true;
userscript.hasRan = true;
} else if (!value) {
if (this.props.instant && typeof userscript.unload === 'function') {
userscript.unload();
} else {
elem.querySelector('.setting-desc-new').textContent = refreshToUnloadMessage;
target.setAttribute('disabled', '');
this.#disabled = true;
if (typeof value === 'boolean') {
let refreshSettings = false;
if ('userscriptReference' in this.props) {
const userscript = this.props.userscriptReference;

// either the userscsript has not ran yet, or it's instant
if (value && (!userscript.hasRan || this.props.instant)) {
userscript.load();
if (!userscript.hasRan) refreshSettings = true;
userscript.hasRan = true;
} else if (!value) {
if (this.props.instant && typeof userscript.unload === 'function') {
userscript.unload();
} else {
elem.querySelector('.setting-desc-new').textContent = refreshToUnloadMessage;
target.setAttribute('disabled', '');
this.#disabled = true;
}
}
ipcRenderer.send('logMainConsole', `userscript: recieved an update for ${userscript.name}: ${value}`);
su.userscriptTracker[userscript.name] = value;
} else {
ipcRenderer.send('logMainConsole', `userscript: recieved an update for ${this.props.title}: ${value}`);
su.userscriptTracker[this.props.title] = value;
}
ipcRenderer.send('logMainConsole', `userscript: recieved an update for ${userscript.name}: ${value}`);
su.userscriptTracker[userscript.name] = value;
saveUserscriptTracker();

// krunkers transition takes .4s, this is more reliable than to wait for transitionend
if (refreshSettings) setTimeout(renderSettings, 400);
} else {
ipcRenderer.send('logMainConsole', `userscript: recieved an update for ${this.props.title}: ${value}`);
su.userscriptTracker[this.props.title] = value;
throw `Callback cannot be "userscript" for non-boolean values, like: ${ value.toString()}`;
}
saveUserscriptTracker();

// krunkers transition takes .4s, this is more reliable than to wait for transitionend
if (refreshSettings) setTimeout(renderSettings, 400);
} else {
// eslint-disable-next-line callback-return
callback(value);
Expand All @@ -319,7 +337,6 @@ class SettingElem {
get elem() {
if (this.#wrapper !== false) return this.#wrapper; // returnt he element if already initialized


// i only create the element after .elem is called so i don't pollute the dom with virutal elements when making settings
const wrapper = createElement('div', {
class: ['setting', 'settName', `safety-${this.props.safety}`, this.props.type],
Expand Down Expand Up @@ -358,7 +375,6 @@ const skeleton = {
* make a setting with some text (notice)
* @param desc description of the notice
* @param opts desc => description, iconHTML => icon's html, generate through skeleton's *icon methods
*
*/
notice: (notice: string, opts?: { desc?: string, iconHTML?: string }) => `
<div class="settName setting">
Expand Down
9 changes: 8 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,11 @@ export function secondsToTimestring(num: number) {

if (minutes < 1) return `${num}s`;
return `${minutes}m ${seconds}s`;
}
}

// https://www.30secondsofcode.org/js/s/arrays-have-same-contents/
export const haveSameContents = (array1: any[], array2: any[]) => {
for (const value of new Set([...array1, ...array2])) if (array1.filter(e => e === value).length !== array2.filter(e => e === value).length) return false;

return true;
};

0 comments on commit 9d86189

Please sign in to comment.