Compare commits
	
		
			No commits in common. "2688e86b9daaa2468b9ba89bcac3ab8912ee5f1a" and "23b92f6f639de353398b27609ab79a4067a3fd14" have entirely different histories. 
		
	
	
		
			2688e86b9d
			...
			23b92f6f63
		
	
		
	
								
									
									
										
											127
										
									
									build.js
									
									
									
									
								
								
							
							
										
											127
										
									
									build.js
									
									
									
									
								| 
						 | 
					@ -1,21 +1,17 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
import beautifier from 'js-beautify';
 | 
					import beautifier from 'js-beautify';
 | 
				
			||||||
const { js: beautify } = beautifier;
 | 
					const { js: beautify } = beautifier;
 | 
				
			||||||
import UglifyJS from 'uglify-js';
 | 
					import UglifyJS from 'uglify-js';
 | 
				
			||||||
import fs from 'fs';
 | 
					import fs from 'fs';
 | 
				
			||||||
import webpack from 'webpack';
 | 
					import webpack from 'webpack';
 | 
				
			||||||
import path from 'path';
 | 
					import path from 'path';
 | 
				
			||||||
import applyPatches from './patches/patches.js';
 | 
					import applyPatches from './patches.js';
 | 
				
			||||||
import ModUtils, { minifyCode } from './modUtils.js';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
 | 
					if (!fs.existsSync("./build")) fs.mkdirSync("./build");
 | 
				
			||||||
fs.cpSync("./static/", "./build/", { recursive: true });
 | 
					fs.cpSync("./static/", "./build/", { recursive: true });
 | 
				
			||||||
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
 | 
					fs.cpSync("./assets/", "./build/assets/", { recursive: true });
 | 
				
			||||||
const buildTimestamp = Date.now().toString();
 | 
					fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
 | 
				
			||||||
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, buildTimestamp));
 | 
					 | 
				
			||||||
fs.writeFileSync("./build/sw.js", fs.readFileSync("./build/sw.js").toString().replace("buildTimestamp", buildTimestamp));
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => {
 | 
					const buildClientCode = () => new Promise((resolve, reject) => {
 | 
				
			||||||
	webpack({
 | 
						webpack({
 | 
				
			||||||
		mode: 'production',
 | 
							mode: 'production',
 | 
				
			||||||
		entry: { fxClient: "./src/main.js" },
 | 
							entry: { fxClient: "./src/main.js" },
 | 
				
			||||||
| 
						 | 
					@ -36,9 +32,9 @@ const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve,
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
		else resolve();
 | 
							else resolve();
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
}));
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).trim();
 | 
					let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const exposeVarsToGlobalScope = true;
 | 
					const exposeVarsToGlobalScope = true;
 | 
				
			||||||
// need to first remove the iife wrapper so the top-level functions aren't inlined
 | 
					// need to first remove the iife wrapper so the top-level functions aren't inlined
 | 
				
			||||||
| 
						 | 
					@ -54,28 +50,93 @@ if (stringArrayRaw === undefined) throw new Error("cannot find the string array"
 | 
				
			||||||
const stringArray = JSON.parse(stringArrayRaw);
 | 
					const stringArray = JSON.parse(stringArrayRaw);
 | 
				
			||||||
script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[index]}"`);
 | 
					script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[index]}"`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const modUtils = new ModUtils(minifyCode(script));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
import customLobbyPatches from './patches/customLobby.js';
 | 
					 | 
				
			||||||
customLobbyPatches(modUtils);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// for versions ^1.99.5.2
 | 
					// for versions ^1.99.5.2
 | 
				
			||||||
const minificationResult = UglifyJS.minify(modUtils.script, {
 | 
					const minificationResult = UglifyJS.minify(script, {
 | 
				
			||||||
	"compress": { "arrows": false },
 | 
						"compress": { "arrows": false },
 | 
				
			||||||
	"mangle": false
 | 
						"mangle": false
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
if (minificationResult.error) {
 | 
					if (minificationResult.error) console.log(minificationResult.error);
 | 
				
			||||||
	console.log("error while passing through UglifyJS, replaceCode replacements might have caused errors");
 | 
					 | 
				
			||||||
	throw minificationResult.error;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
if (minificationResult.warnings) console.log(minificationResult.warnings);
 | 
					if (minificationResult.warnings) console.log(minificationResult.warnings);
 | 
				
			||||||
modUtils.script = minificationResult.code;
 | 
					script = minificationResult.code;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const {
 | 
					const replaceOne = (expression, replaceValue) => {
 | 
				
			||||||
    matchDictionaryExpression,
 | 
					    const result = matchOne(expression);
 | 
				
			||||||
    generateRegularExpression
 | 
					    // this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
 | 
				
			||||||
} = modUtils;
 | 
					    script = script.replace(expression, replaceValue);
 | 
				
			||||||
const dictionary = modUtils.dictionary;
 | 
					    return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const replace = (...args) => script = script.replace(...args);
 | 
				
			||||||
 | 
					const matchOne = (expression) => {
 | 
				
			||||||
 | 
						const result = expression.exec(script);
 | 
				
			||||||
 | 
					    if (result === null) throw new Error("no match for: ") + expression;
 | 
				
			||||||
 | 
						if (expression.exec(script) !== null) throw new Error("more than one match for: " + expression);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					// https://stackoverflow.com/a/63838890
 | 
				
			||||||
 | 
					const escapeRegExp = (string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					//const dictionary = { __dictionaryVersion: '1.90.0   4 Feb 2024', playerId: 'bB', playerNames: 'hA', playerBalances: 'bC', playerTerritories: 'bj', gIsSingleplayer: 'fc', gIsTeamGame: 'cH' };
 | 
				
			||||||
 | 
					//if (!script.includes(`"${dictionary.__dictionaryVersion}"`)) throw new Error("Dictionary is outdated.");
 | 
				
			||||||
 | 
					const dictionary = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const matchDictionaryExpression = expression => {
 | 
				
			||||||
 | 
						const result = expression.exec(script);
 | 
				
			||||||
 | 
						if (result === null) throw new Error("no match for ") + expression;
 | 
				
			||||||
 | 
						if (expression.exec(script) !== null) throw new Error("more than one match for: ") + expression;
 | 
				
			||||||
 | 
						for (let [key, value] of Object.entries(result.groups)) dictionary[key] = value;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Return value example:
 | 
				
			||||||
 | 
					// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code
 | 
				
			||||||
 | 
					// and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
 | 
				
			||||||
 | 
					const replaceRawCode = (/** @type {string} */ raw, /** @type {string} */ result, nameMappings) => {
 | 
				
			||||||
 | 
						const { expression, groups } = generateRegularExpression(raw, false, nameMappings);
 | 
				
			||||||
 | 
						let localizerCount = 0;
 | 
				
			||||||
 | 
						let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
 | 
				
			||||||
 | 
						.replace(/\w+/g, match => {
 | 
				
			||||||
 | 
							// these would get stored as "___localizer1", "___localizer2", ...
 | 
				
			||||||
 | 
							if (match === "__L") match = "___localizer" + (++localizerCount);
 | 
				
			||||||
 | 
							return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						//console.log(replacementString);
 | 
				
			||||||
 | 
						let expressionMatchResult;
 | 
				
			||||||
 | 
						try { expressionMatchResult = replaceOne(expression, replacementString); }
 | 
				
			||||||
 | 
						catch (e) {
 | 
				
			||||||
 | 
							throw new Error("replaceRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const matchRawCode = (/** @type {string} */ raw, nameMappings) => {
 | 
				
			||||||
 | 
						const { expression, groups } = generateRegularExpression(raw, false, nameMappings);
 | 
				
			||||||
 | 
						const expressionMatchResult = matchOne(expression);
 | 
				
			||||||
 | 
						return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => {
 | 
				
			||||||
 | 
						const groups = {};
 | 
				
			||||||
 | 
						let groupNumberCounter = 1;
 | 
				
			||||||
 | 
						let localizerCount = 0;
 | 
				
			||||||
 | 
						let raw = escapeRegExp(code).replaceAll("__L\\(\\)", "___localizer\\)")
 | 
				
			||||||
 | 
							// when there is a parameter, add a comma to separate it from the added number
 | 
				
			||||||
 | 
							.replaceAll("__L\\(", "___localizer,");
 | 
				
			||||||
 | 
						raw = raw.replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
 | 
				
			||||||
 | 
							// if a substitution string for the "word" is specified in the nameMappings, use it
 | 
				
			||||||
 | 
							if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
 | 
				
			||||||
 | 
							// if the "word" is a number or is one of these specific words, ingore it
 | 
				
			||||||
 | 
							if (/^\d/.test(word) || ["return", "this", "var", "function", "new", "Math", "WebSocket"].includes(word)) return word;
 | 
				
			||||||
 | 
							// for easy localizer function matching
 | 
				
			||||||
 | 
							else if (word === "___localizer") {
 | 
				
			||||||
 | 
								groups[word + (++localizerCount)] = groupNumberCounter++;
 | 
				
			||||||
 | 
								return "\\b(L\\(\\d+)"; // would match "L(123", "L(50" and etc. when using "__L("
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group
 | 
				
			||||||
 | 
							else {
 | 
				
			||||||
 | 
								groups[word] = groupNumberCounter++;
 | 
				
			||||||
 | 
								return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
						let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
 | 
				
			||||||
 | 
						return { expression, groups };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[
 | 
					[
 | 
				
			||||||
	/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
 | 
						/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
 | 
				
			||||||
| 
						 | 
					@ -100,24 +161,18 @@ rawCodeSegments.forEach(code => {
 | 
				
			||||||
	matchDictionaryExpression(expression);
 | 
						matchDictionaryExpression(expression);
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
modUtils.executePostMinifyHandlers();
 | 
					applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
 | 
				
			||||||
applyPatches(modUtils);
 | 
					 | 
				
			||||||
script = modUtils.script;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
await buildClientCode();
 | 
					await buildClientCode();
 | 
				
			||||||
// the dictionary should maybe get embedded into one of the files in the bundle
 | 
					// the dictionary should maybe get embedded into one of the files in the bundle
 | 
				
			||||||
fs.writeFileSync(
 | 
					fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
 | 
				
			||||||
	"./build/fx.bundle.js",
 | 
					 | 
				
			||||||
	`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
 | 
					 | 
				
			||||||
	+ fs.readFileSync("./build/fx.bundle.js").toString()
 | 
					 | 
				
			||||||
);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
console.log("Formatting code...");
 | 
					console.log("Formatting code...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
script = beautify(script, {
 | 
					script = beautify(script, {
 | 
				
			||||||
	"indent_size": 1,
 | 
						"indent_size": "1",
 | 
				
			||||||
	"indent_char": "\t",
 | 
						"indent_char": "\t",
 | 
				
			||||||
	"max_preserve_newlines": 5,
 | 
						"max_preserve_newlines": "5",
 | 
				
			||||||
	"preserve_newlines": true,
 | 
						"preserve_newlines": true,
 | 
				
			||||||
	"keep_array_indentation": false,
 | 
						"keep_array_indentation": false,
 | 
				
			||||||
	"break_chained_methods": false,
 | 
						"break_chained_methods": false,
 | 
				
			||||||
| 
						 | 
					@ -128,7 +183,7 @@ script = beautify(script, {
 | 
				
			||||||
	"unescape_strings": false,
 | 
						"unescape_strings": false,
 | 
				
			||||||
	"jslint_happy": false,
 | 
						"jslint_happy": false,
 | 
				
			||||||
	"end_with_newline": false,
 | 
						"end_with_newline": false,
 | 
				
			||||||
	"wrap_line_length": 250,
 | 
						"wrap_line_length": "250",
 | 
				
			||||||
	"indent_inner_html": false,
 | 
						"indent_inner_html": false,
 | 
				
			||||||
	"comma_first": false,
 | 
						"comma_first": false,
 | 
				
			||||||
	"e4x": false,
 | 
						"e4x": false,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
								
									
									
										
											134
										
									
									modUtils.js
									
									
									
									
								
								
							
							
										
											134
										
									
									modUtils.js
									
									
									
									
								| 
						 | 
					@ -1,134 +0,0 @@
 | 
				
			||||||
// @ts-check
 | 
					 | 
				
			||||||
import UglifyJS from 'uglify-js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// https://stackoverflow.com/a/63838890
 | 
					 | 
				
			||||||
const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export function minifyCode(/** @type {string} */ script) {
 | 
					 | 
				
			||||||
    const output = UglifyJS.minify(script, {
 | 
					 | 
				
			||||||
        compress: false,
 | 
					 | 
				
			||||||
        mangle: false
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (output.error) throw output.error;
 | 
					 | 
				
			||||||
    if (output.warnings) throw (output.warnings);
 | 
					 | 
				
			||||||
    if (output.warnings) console.warn(output.warnings);
 | 
					 | 
				
			||||||
    return output.code;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class ModUtils {
 | 
					 | 
				
			||||||
    script = "";
 | 
					 | 
				
			||||||
    /** @type {{[key: string]: string}} */
 | 
					 | 
				
			||||||
    dictionary = {};
 | 
					 | 
				
			||||||
    /** @type {Function[]} */
 | 
					 | 
				
			||||||
    postMinifyHandlers = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    constructor(/** @type {string} */ script) {
 | 
					 | 
				
			||||||
        this.script = script;
 | 
					 | 
				
			||||||
        // Bind methods
 | 
					 | 
				
			||||||
        this.matchDictionaryExpression = this.matchDictionaryExpression.bind(this);
 | 
					 | 
				
			||||||
        this.generateRegularExpression = this.generateRegularExpression.bind(this);
 | 
					 | 
				
			||||||
        this.replace = this.replace.bind(this);
 | 
					 | 
				
			||||||
        this.replaceOne = this.replaceOne.bind(this);
 | 
					 | 
				
			||||||
        this.replaceRawCode = this.replaceRawCode.bind(this);
 | 
					 | 
				
			||||||
        this.matchOne = this.matchOne.bind(this);
 | 
					 | 
				
			||||||
        this.matchRawCode = this.matchRawCode.bind(this);
 | 
					 | 
				
			||||||
        this.replaceCode = this.replaceCode.bind(this);
 | 
					 | 
				
			||||||
        this.waitForMinification = this.waitForMinification.bind(this);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    /** @param {RegExp} expression */
 | 
					 | 
				
			||||||
    matchDictionaryExpression(expression) {
 | 
					 | 
				
			||||||
        const result = this.matchOne(expression);
 | 
					 | 
				
			||||||
        // @ts-ignore
 | 
					 | 
				
			||||||
        for (let [key, value] of Object.entries(result.groups)) this.addToDictionary(key, value);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    replace(/** @type {Parameters<typeof String.prototype.replace>} */ ...args) {
 | 
					 | 
				
			||||||
        return this.script = this.script.replace(...args);
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    /** Expressions passed to this function must have the global flag set. */
 | 
					 | 
				
			||||||
    matchOne(/** @type {RegExp} */ expression) {
 | 
					 | 
				
			||||||
        const result = expression.exec(this.script);
 | 
					 | 
				
			||||||
        if (result === null) throw new Error("no match for: " + expression.toString());
 | 
					 | 
				
			||||||
        if (expression.exec(this.script) !== null) throw new Error("more than one match for: " + expression.toString());
 | 
					 | 
				
			||||||
        return result;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    addToDictionary(/** @type {string} */ key, /** @type {string} */ value) {
 | 
					 | 
				
			||||||
        if (this.dictionary[key] !== undefined && this.dictionary[key] !== value)
 | 
					 | 
				
			||||||
            throw new Error("name different from existing one:\n  KEY: " + key + "\n  VALUE: " + value + "\n  Value in dictionary: " + this.dictionary[key]);
 | 
					 | 
				
			||||||
        this.dictionary[key] = value;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * @param {RegExp} expression
 | 
					 | 
				
			||||||
     * @param {string} replaceValue
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    replaceOne(expression, replaceValue) {
 | 
					 | 
				
			||||||
        const result = this.matchOne(expression);
 | 
					 | 
				
			||||||
        // this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
 | 
					 | 
				
			||||||
        this.script = this.script.replace(expression, replaceValue);
 | 
					 | 
				
			||||||
        return result;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Return value example:
 | 
					 | 
				
			||||||
    // When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code
 | 
					 | 
				
			||||||
    // and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
 | 
					 | 
				
			||||||
    replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) {
 | 
					 | 
				
			||||||
        const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
 | 
					 | 
				
			||||||
        let localizerCount = 0;
 | 
					 | 
				
			||||||
        let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
 | 
					 | 
				
			||||||
            .replace(/\w+/g, match => {
 | 
					 | 
				
			||||||
                // these would get stored as "___localizer1", "___localizer2", ...
 | 
					 | 
				
			||||||
                if (match === "__L") match = "___localizer" + (++localizerCount);
 | 
					 | 
				
			||||||
                return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        //console.log(replacementString);
 | 
					 | 
				
			||||||
        let expressionMatchResult;
 | 
					 | 
				
			||||||
        try { expressionMatchResult = this.replaceOne(expression, replacementString); }
 | 
					 | 
				
			||||||
        catch (e) {
 | 
					 | 
				
			||||||
            throw new Error("replaceRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    matchRawCode(/** @type {string} */ raw, nameMappings) {
 | 
					 | 
				
			||||||
        const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
 | 
					 | 
				
			||||||
        const expressionMatchResult = this.matchOne(expression);
 | 
					 | 
				
			||||||
        return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) {
 | 
					 | 
				
			||||||
        const groups = {};
 | 
					 | 
				
			||||||
        let groupNumberCounter = 1;
 | 
					 | 
				
			||||||
        let localizerCount = 0;
 | 
					 | 
				
			||||||
        let raw = escapeRegExp(code).replaceAll("__L\\(\\)", "___localizer\\)")
 | 
					 | 
				
			||||||
            // when there is a parameter, add a comma to separate it from the added number
 | 
					 | 
				
			||||||
            .replaceAll("__L\\(", "___localizer,");
 | 
					 | 
				
			||||||
        raw = raw.replace(isForDictionary ? /(?:@@)*(@?)(\w+)/g : /()(\w+)/g, (_match, modifier, word) => {
 | 
					 | 
				
			||||||
            // if a substitution string for the "word" is specified in the nameMappings, use it
 | 
					 | 
				
			||||||
            if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
 | 
					 | 
				
			||||||
            // if the "word" is a number or is one of these specific words, ingore it
 | 
					 | 
				
			||||||
            if (/^\d/.test(word) || ["return", "this", "var", "function", "new", "Math", "WebSocket"].includes(word)) return word;
 | 
					 | 
				
			||||||
            // for easy localizer function matching
 | 
					 | 
				
			||||||
            else if (word === "___localizer") {
 | 
					 | 
				
			||||||
                groups[word + (++localizerCount)] = groupNumberCounter++;
 | 
					 | 
				
			||||||
                return "\\b(L\\(\\d+)"; // would match "L(123", "L(50" and etc. when using "__L("
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group
 | 
					 | 
				
			||||||
            else {
 | 
					 | 
				
			||||||
                groups[word] = groupNumberCounter++;
 | 
					 | 
				
			||||||
                return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
 | 
					 | 
				
			||||||
        return { expression, groups };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    replaceCode(code, replacement, options) {
 | 
					 | 
				
			||||||
        return this.replaceRawCode(minifyCode(code), replacement);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    waitForMinification(/** @type {Function} */ handler) {
 | 
					 | 
				
			||||||
        this.postMinifyHandlers.push(handler);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    executePostMinifyHandlers() {
 | 
					 | 
				
			||||||
        this.postMinifyHandlers.forEach(handler => handler());
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    escapeRegExp = escapeRegExp
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default ModUtils;
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,5 @@
 | 
				
			||||||
import assets from '../assets.js';
 | 
					import assets from './assets.js';
 | 
				
			||||||
import ModUtils from '../modUtils.js';
 | 
					export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
 | 
				
			||||||
export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Constants for easy usage of otherwise long variable access expressions
 | 
					    // Constants for easy usage of otherwise long variable access expressions
 | 
				
			||||||
    const dict = dictionary;
 | 
					    const dict = dictionary;
 | 
				
			||||||
| 
						 | 
					@ -303,6 +302,66 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    { // Custom lobbies
 | 
				
			||||||
 | 
					        replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}",
 | 
				
			||||||
 | 
					            `this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()},
 | 
				
			||||||
 | 
					            __fx.customLobby.setJoinFunction(() => { i___.rX(); aM.a7U(0); aM.init(); })`
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        replaceRawCode(`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(url)`,
 | 
				
			||||||
 | 
					            `(socketId-aq.kt.a82)+"/",(socket=new WebSocket(__fx.customLobby.isActive() && socketId === 1 ? __fx.customLobby.getSocketURL() : url)`)
 | 
				
			||||||
 | 
					        replaceRawCode("this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)}",
 | 
				
			||||||
 | 
					            "this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)},__fx.customLobby.setSendFunction(this.send)")
 | 
				
			||||||
 | 
					        replaceRawCode("b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):",
 | 
				
			||||||
 | 
					            "b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):__fx.customLobby.isCustomMessage(a0)||")
 | 
				
			||||||
 | 
					        // set the custom lobby to inactive when clicking the "Back" button on the connection screen or leaving the lobby
 | 
				
			||||||
 | 
					        replaceRawCode("this.xZ=function(){Sockets.kt.wf(3260),i___.kt.we()}",
 | 
				
			||||||
 | 
					            "this.xZ=function(){Sockets.kt.wf(3260),__fx.customLobby.setActive(false),i___.kt.we()}")
 | 
				
			||||||
 | 
					        replaceRawCode("function(){n.r(),bl.zf(),Sockets.s.ze(3240),n.o(5,5)}",
 | 
				
			||||||
 | 
					            `(__fx.customLobby.setLeaveFunction(() => {n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)}),
 | 
				
			||||||
 | 
					            function(){n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)})`)
 | 
				
			||||||
 | 
					        // when a socket error occurs on the custom lobby socket
 | 
				
			||||||
 | 
					        replaceRawCode("this.wQ=function(wR,d){if(8===i.pz&&0===wR)if(4211===d)wS(d);",
 | 
				
			||||||
 | 
					            `this.wQ=function(wR,d){
 | 
				
			||||||
 | 
					            wR===1 && __fx.customLobby.isActive() && ${dict.MenuManager}.${dict.getState}() !== 6 && __fx.customLobby.setActive(false);
 | 
				
			||||||
 | 
					            if(8===i.pz&&0===wR)if(4211===d)wS(d);`)
 | 
				
			||||||
 | 
					        // when leaving a game
 | 
				
			||||||
 | 
					        replaceRawCode("this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),ap.ky.zt(),this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0),2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}",
 | 
				
			||||||
 | 
					            `this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),
 | 
				
			||||||
 | 
					            __fx.customLobby.isActive() === false && ap.ky.zt(),
 | 
				
			||||||
 | 
					            this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0);
 | 
				
			||||||
 | 
					            if (__fx.customLobby.isActive()) __fx.customLobby.rejoinLobby(); else 2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}`)
 | 
				
			||||||
 | 
					        // do not display lobby UI
 | 
				
			||||||
 | 
					        replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`,
 | 
				
			||||||
 | 
					            `(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),
 | 
				
			||||||
 | 
					            __fx.customLobby.isActive() && (sV.style.display = "none"),`);
 | 
				
			||||||
 | 
					        // allow games with one player
 | 
				
			||||||
 | 
					        replaceRawCode("if((t3=bk.t1.t3[e0])<2)return!1;", "if((t3=bk.t1.t3[e0])<2 && !__fx.customLobby.isActive())return!1;")
 | 
				
			||||||
 | 
					        // if the server is unreachable
 | 
				
			||||||
 | 
					        replaceRawCode("{g.wc(3249)}", "{__fx.customLobby.isActive()?(g.wc(3249),__fx.customLobby.setActive(false)):g.wc(3249)}")
 | 
				
			||||||
 | 
					        // error descriptions
 | 
				
			||||||
 | 
					        const errors = { 3249: "No servers found", 4705: "Lobby not found", 4730: "Kicked from lobby" };
 | 
				
			||||||
 | 
					        replaceRawCode(`m.n___(4,5,new o(__L(),xT(e),!0))`,
 | 
				
			||||||
 | 
					            `m.n___(4,5,new o(__L(),${JSON.stringify(errors)}[e] ?? xT(e),!0))`)
 | 
				
			||||||
 | 
					        // map info (for the map selection menu)
 | 
				
			||||||
 | 
					        replaceRawCode("this.info=new Array(Maps.totalMapCount+1),this.info[0]={name:__L(),",
 | 
				
			||||||
 | 
					            "this.info=new Array(Maps.totalMapCount+1),__fx.customLobby.setMapInfo(this.info),this.info[0]={name:__L(),")
 | 
				
			||||||
 | 
					        // to not set custom lobby games as singleplayer
 | 
				
			||||||
 | 
					        replaceRawCode("this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK,",
 | 
				
			||||||
 | 
					            "this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK&&!__fx.customLobby.isActive(),")
 | 
				
			||||||
 | 
					        // custom difficulty
 | 
				
			||||||
 | 
					        replaceRawCode("if(9===a1.jq)this.jr();else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=",
 | 
				
			||||||
 | 
					            `if(9===a1.jq)this.jr();
 | 
				
			||||||
 | 
					            else if (__fx.customLobby.isActive()) for(z=a1.ju-1;0<=z;z--) this.ie[z+jp] = __fx.customLobby.gameInfo.difficulty;
 | 
				
			||||||
 | 
					            else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=`)
 | 
				
			||||||
 | 
					        // spawn selection
 | 
				
			||||||
 | 
					        replaceRawCode(":50,this.a=this.b=this.data.c,this.d=this.b?new e:null,",
 | 
				
			||||||
 | 
					            ":50,this.a=this.b=__fx.customLobby.isActive() ? __fx.customLobby.gameInfo.spawnSelection : this.data.c,this.d=this.b?new e:null,")
 | 
				
			||||||
 | 
					        // bot count
 | 
				
			||||||
 | 
					        replaceRawCode(",this.gLobbyMaxJoin=1===dg?this.gHumans:this.data.playerCount,this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,",
 | 
				
			||||||
 | 
					            `,this.gLobbyMaxJoin = __fx.customLobby.isActive() ? Math.max(Math.min(__fx.customLobby.gameInfo.botCount, this.data.playerCount), this.gHumans) : 1===dg?this.gHumans:this.data.playerCount,
 | 
				
			||||||
 | 
					            this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,`)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Detailed team pie chart percentage
 | 
					    // Detailed team pie chart percentage
 | 
				
			||||||
    replaceRawCode(`qr=Math.floor(100*f0+.5)+"%"`,
 | 
					    replaceRawCode(`qr=Math.floor(100*f0+.5)+"%"`,
 | 
				
			||||||
        `qr = (__fx.settings.detailedTeamPercentage ? (100*f0).toFixed(2) : Math.floor(100*f0+.5)) + "%"`)
 | 
					        `qr = (__fx.settings.detailedTeamPercentage ? (100*f0).toFixed(2) : Math.floor(100*f0+.5)) + "%"`)
 | 
				
			||||||
| 
						 | 
					@ -1,81 +0,0 @@
 | 
				
			||||||
import ModUtils from '../modUtils.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Custom lobby patches
 | 
					 | 
				
			||||||
export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionary: dict, waitForMinification }) => {
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // set player id correctly
 | 
					 | 
				
			||||||
    replaceCode(`function aBG(aBE) {
 | 
					 | 
				
			||||||
		if (!Lobby.aAl) { return -1; }
 | 
					 | 
				
			||||||
		var s = aBE.length;
 | 
					 | 
				
			||||||
		var qu = Lobby.aAl.qu;
 | 
					 | 
				
			||||||
		for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
 | 
					 | 
				
			||||||
		return -1;
 | 
					 | 
				
			||||||
	}`, `function aBG(aBE) {
 | 
					 | 
				
			||||||
		if (!Lobby.aAl) { return -1; }
 | 
					 | 
				
			||||||
        if (__fx.customLobby.isActive()) return __fx.customLobby.getPlayerId();
 | 
					 | 
				
			||||||
		var s = aBE.length;
 | 
					 | 
				
			||||||
		var qu = Lobby.aAl.qu;
 | 
					 | 
				
			||||||
		for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
 | 
					 | 
				
			||||||
		return -1;
 | 
					 | 
				
			||||||
	}`);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    waitForMinification(() => {
 | 
					 | 
				
			||||||
        replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}",
 | 
					 | 
				
			||||||
            `this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()},
 | 
					 | 
				
			||||||
            __fx.customLobby.setJoinFunction(() => { i___.rX(); aM.a7U(0); aM.init(); })`
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
        replaceRawCode(`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(url)`,
 | 
					 | 
				
			||||||
            `(socketId-aq.kt.a82)+"/",(socket=new WebSocket(__fx.customLobby.isActive() && socketId === 1 ? __fx.customLobby.getSocketURL() : url)`)
 | 
					 | 
				
			||||||
        replaceRawCode("this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)}",
 | 
					 | 
				
			||||||
            "this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)},__fx.customLobby.setSendFunction(this.send)")
 | 
					 | 
				
			||||||
        replaceRawCode("b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):",
 | 
					 | 
				
			||||||
            "b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):__fx.customLobby.isCustomMessage(a0)||")
 | 
					 | 
				
			||||||
        // set the custom lobby to inactive when clicking the "Back" button on the connection screen or leaving the lobby
 | 
					 | 
				
			||||||
        replaceRawCode("this.xZ=function(){Sockets.kt.wf(3260),i___.kt.we()}",
 | 
					 | 
				
			||||||
            "this.xZ=function(){Sockets.kt.wf(3260),__fx.customLobby.setActive(false),i___.kt.we()}")
 | 
					 | 
				
			||||||
        replaceRawCode("function(){n.r(),bl.zf(),Sockets.s.ze(3240),n.o(5,5)}",
 | 
					 | 
				
			||||||
            `(__fx.customLobby.setLeaveFunction(() => {n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)}),
 | 
					 | 
				
			||||||
            function(){n.r(),bl.zf(),Sockets.s.ze(3240),__fx.customLobby.setActive(false),n.o(5,5)})`)
 | 
					 | 
				
			||||||
        // when a socket error occurs on the custom lobby socket
 | 
					 | 
				
			||||||
        replaceRawCode("this.wQ=function(wR,d){if(8===i.pz&&0===wR)if(4211===d)wS(d);",
 | 
					 | 
				
			||||||
            `this.wQ=function(wR,d){
 | 
					 | 
				
			||||||
            wR===1 && __fx.customLobby.isActive() && ${dict.MenuManager}.${dict.getState}() !== 6 && __fx.customLobby.setActive(false);
 | 
					 | 
				
			||||||
            if(8===i.pz&&0===wR)if(4211===d)wS(d);`)
 | 
					 | 
				
			||||||
        // when leaving a game
 | 
					 | 
				
			||||||
        replaceRawCode("this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),ap.ky.zt(),this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0),2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}",
 | 
					 | 
				
			||||||
            `this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),
 | 
					 | 
				
			||||||
            __fx.customLobby.isActive() === false && ap.ky.zt(),
 | 
					 | 
				
			||||||
            this.vH=0,bU.zu(),m.n.setState(0),zs||bJ.df.show(),aN.setState(0);
 | 
					 | 
				
			||||||
            if (__fx.customLobby.isActive()) __fx.customLobby.rejoinLobby(); else 2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}`)
 | 
					 | 
				
			||||||
        // do not display lobby UI
 | 
					 | 
				
			||||||
        replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`,
 | 
					 | 
				
			||||||
            `(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),
 | 
					 | 
				
			||||||
            __fx.customLobby.isActive() && (sV.style.display = "none"),`);
 | 
					 | 
				
			||||||
        // allow games with one player
 | 
					 | 
				
			||||||
        replaceRawCode("if((t3=bk.t1.t3[e0])<2)return!1;", "if((t3=bk.t1.t3[e0])<2 && !__fx.customLobby.isActive())return!1;")
 | 
					 | 
				
			||||||
        // if the server is unreachable
 | 
					 | 
				
			||||||
        replaceRawCode("{g.wc(3249)}", "{__fx.customLobby.isActive()?(g.wc(3249),__fx.customLobby.setActive(false)):g.wc(3249)}")
 | 
					 | 
				
			||||||
        // error descriptions
 | 
					 | 
				
			||||||
        const errors = { 3249: "No servers found", 4705: "Lobby not found", 4730: "Kicked from lobby" };
 | 
					 | 
				
			||||||
        replaceRawCode(`m.n___(4,5,new o(__L(),xT(e),!0))`,
 | 
					 | 
				
			||||||
            `m.n___(4,5,new o(__L(),${JSON.stringify(errors)}[e] ?? xT(e),!0))`)
 | 
					 | 
				
			||||||
        // map info (for the map selection menu)
 | 
					 | 
				
			||||||
        replaceRawCode("this.info=new Array(Maps.totalMapCount+1),this.info[0]={name:__L(),",
 | 
					 | 
				
			||||||
            "this.info=new Array(Maps.totalMapCount+1),__fx.customLobby.setMapInfo(this.info),this.info[0]={name:__L(),")
 | 
					 | 
				
			||||||
        // to not set custom lobby games as singleplayer
 | 
					 | 
				
			||||||
        replaceRawCode("this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK,",
 | 
					 | 
				
			||||||
            "this.vK=this.jS=this.data.a0f,this.gameIsSingleplayer=1===this.vK&&!__fx.customLobby.isActive(),")
 | 
					 | 
				
			||||||
        // custom difficulty
 | 
					 | 
				
			||||||
        replaceRawCode("if(9===a1.jq)this.jr();else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=",
 | 
					 | 
				
			||||||
            `if(9===a1.jq)this.jr();
 | 
					 | 
				
			||||||
            else if (__fx.customLobby.isActive()) for(z=a1.ju-1;0<=z;z--) this.ie[z+jp] = __fx.customLobby.gameInfo.difficulty;
 | 
					 | 
				
			||||||
            else if(a1.js)if(3===a1.data.jv)for(z=a1.ju-1;0<=z;z--){var jw=z+jp;this.ie[jw]=`)
 | 
					 | 
				
			||||||
        // spawn selection
 | 
					 | 
				
			||||||
        replaceRawCode(":50,this.a=this.b=this.data.c,this.d=this.b?new e:null,",
 | 
					 | 
				
			||||||
            ":50,this.a=this.b=__fx.customLobby.isActive() ? __fx.customLobby.gameInfo.spawnSelection : this.data.c,this.d=this.b?new e:null,")
 | 
					 | 
				
			||||||
        // bot count
 | 
					 | 
				
			||||||
        replaceRawCode(",this.gLobbyMaxJoin=1===dg?this.gHumans:this.data.playerCount,this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,",
 | 
					 | 
				
			||||||
            `,this.gLobbyMaxJoin = __fx.customLobby.isActive() ? Math.max(Math.min(__fx.customLobby.gameInfo.botCount, this.data.playerCount), this.gHumans) : 1===dg?this.gHumans:this.data.playerCount,
 | 
					 | 
				
			||||||
            this.maxPlayers=this.gLobbyMaxJoin,this.gBots=this.gLobbyMaxJoin-this.gHumans,this.sg=0,`)
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -30,8 +30,8 @@ main.className = "customlobby-main";
 | 
				
			||||||
const playerListContainer = document.createElement("div");
 | 
					const playerListContainer = document.createElement("div");
 | 
				
			||||||
const playerCount = document.createElement("p");
 | 
					const playerCount = document.createElement("p");
 | 
				
			||||||
playerCount.textContent = "0 Players";
 | 
					playerCount.textContent = "0 Players";
 | 
				
			||||||
const playerListDiv = document.createElement("div");
 | 
					const playerList = document.createElement("div");
 | 
				
			||||||
playerListContainer.append(playerCount, playerListDiv);
 | 
					playerListContainer.append(playerCount, playerList);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const optionsContainer = document.createElement("div");
 | 
					const optionsContainer = document.createElement("div");
 | 
				
			||||||
optionsContainer.className = "text-align-left";
 | 
					optionsContainer.className = "text-align-left";
 | 
				
			||||||
| 
						 | 
					@ -195,46 +195,40 @@ function isCustomMessage(raw) {
 | 
				
			||||||
        Object.entries(data.options).forEach(([option, value]) => updateOption(option, value));
 | 
					        Object.entries(data.options).forEach(([option, value]) => updateOption(option, value));
 | 
				
			||||||
        displayPlayers(data.players);
 | 
					        displayPlayers(data.players);
 | 
				
			||||||
    } else if (type === "addPlayer") {
 | 
					    } else if (type === "addPlayer") {
 | 
				
			||||||
        addPlayer({ name: data.name, inGame: false, isHost: false });
 | 
					        addPlayer(data);
 | 
				
			||||||
        updatePlayerCount();
 | 
					        updatePlayerCount();
 | 
				
			||||||
    } else if (type === "removePlayer") {
 | 
					    } else if (type === "removePlayer") {
 | 
				
			||||||
        const index = data;
 | 
					        const index = data;
 | 
				
			||||||
        playerList[index].element.remove();
 | 
					        playerElements[index].element.remove();
 | 
				
			||||||
        playerList.splice(index, 1);
 | 
					        playerElements.splice(index, 1);
 | 
				
			||||||
        updatePlayerCount();
 | 
					        updatePlayerCount();
 | 
				
			||||||
    } else if (type === "inLobby") {
 | 
					    } else if (type === "inLobby") {
 | 
				
			||||||
        const index = data;
 | 
					        const index = data;
 | 
				
			||||||
        playerList[index].inGameBadge.className = "d-none";
 | 
					        playerElements[index].inGameBadge.className = "d-none";
 | 
				
			||||||
    } else if (type === "options") {
 | 
					    } else if (type === "options") {
 | 
				
			||||||
        const [option, value] = data;
 | 
					        const [option, value] = data;
 | 
				
			||||||
        updateOption(option, value);
 | 
					        updateOption(option, value);
 | 
				
			||||||
    } else if (type === "setHost") {
 | 
					    } else if (type === "setHost") {
 | 
				
			||||||
        const index = data;
 | 
					        const index = data;
 | 
				
			||||||
        playerList[index].isHost = true;
 | 
					        playerElements[index].isHost = true;
 | 
				
			||||||
        playerList[index].hostBadge.className = "";
 | 
					        playerElements[index].hostBadge.className = "";
 | 
				
			||||||
    } else if (type === "host") {
 | 
					    } else if (type === "host") {
 | 
				
			||||||
        playerIsHost = true;
 | 
					        playerIsHost = true;
 | 
				
			||||||
        startButton.disabled = false;
 | 
					        startButton.disabled = false;
 | 
				
			||||||
        optionsContainer.classList.remove("disabled");
 | 
					        optionsContainer.classList.remove("disabled");
 | 
				
			||||||
        playerList.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
 | 
					        playerElements.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
 | 
				
			||||||
    } else if (type === "serverMessage") alert(data);
 | 
					    } else if (type === "serverMessage") alert(data);
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					/** @type {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean }[]} */
 | 
				
			||||||
/** @typedef {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean, inGame: boolean }} PlayerListEntry */
 | 
					let playerElements = [];
 | 
				
			||||||
 | 
					 | 
				
			||||||
/** @type {PlayerListEntry[]} */
 | 
					 | 
				
			||||||
let playerList = [];
 | 
					 | 
				
			||||||
/** @type {PlayerListEntry} */
 | 
					 | 
				
			||||||
let thisPlayer;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
function createBadge(text, visible) {
 | 
					function createBadge(text, visible) {
 | 
				
			||||||
    const badge = document.createElement("span");
 | 
					    const badge = document.createElement("span");
 | 
				
			||||||
    badge.textContent = text;
 | 
					    badge.textContent = text;
 | 
				
			||||||
    badge.className = visible ? "" : "d-none";
 | 
					    badge.className = visible ? "" : "d-none";
 | 
				
			||||||
    return badge;
 | 
					    return badge;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/** @typedef {{ name: string, isHost: boolean, inGame: boolean }} PlayerInfo */
 | 
					/** @typedef {{ name: string, isHost?: boolean, inGame?: boolean }} PlayerInfo */
 | 
				
			||||||
/** @param {PlayerInfo} player */
 | 
					/** @param {PlayerInfo} player */
 | 
				
			||||||
function addPlayer(player) {
 | 
					function addPlayer(player) {
 | 
				
			||||||
    const div = document.createElement("div");
 | 
					    const div = document.createElement("div");
 | 
				
			||||||
| 
						 | 
					@ -247,13 +241,13 @@ function addPlayer(player) {
 | 
				
			||||||
    const hostBadge = createBadge("Host", player.isHost);
 | 
					    const hostBadge = createBadge("Host", player.isHost);
 | 
				
			||||||
    const inGameBadge = createBadge("In Game", player.inGame);
 | 
					    const inGameBadge = createBadge("In Game", player.inGame);
 | 
				
			||||||
    div.append(hostBadge, inGameBadge, kickButton);
 | 
					    div.append(hostBadge, inGameBadge, kickButton);
 | 
				
			||||||
    playerListDiv.append(div);
 | 
					    playerList.append(div);
 | 
				
			||||||
    playerList.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost, inGame: player.inGame });
 | 
					    playerElements.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function kickButtonHandler(event) {
 | 
					function kickButtonHandler(event) {
 | 
				
			||||||
    const button = event.target;
 | 
					    const button = event.target;
 | 
				
			||||||
    for (let index = 0; index < playerList.length; index++) {
 | 
					    for (let index = 0; index < playerElements.length; index++) {
 | 
				
			||||||
        if (playerList[index].kickButton === button) {
 | 
					        if (playerElements[index].kickButton === button) {
 | 
				
			||||||
            sendMessage("kick", index);
 | 
					            sendMessage("kick", index);
 | 
				
			||||||
            break;
 | 
					            break;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					@ -261,27 +255,18 @@ function kickButtonHandler(event) {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
/** @param {PlayerInfo[]} players */
 | 
					/** @param {PlayerInfo[]} players */
 | 
				
			||||||
function displayPlayers(players) {
 | 
					function displayPlayers(players) {
 | 
				
			||||||
    playerList = [];
 | 
					    playerElements = [];
 | 
				
			||||||
    playerListDiv.innerHTML = "";
 | 
					    playerList.innerHTML = "";
 | 
				
			||||||
    players.forEach(addPlayer);
 | 
					    players.forEach(addPlayer);
 | 
				
			||||||
    thisPlayer = playerList[playerList.length - 1];
 | 
					 | 
				
			||||||
    updatePlayerCount();
 | 
					    updatePlayerCount();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function updatePlayerCount() {
 | 
					function updatePlayerCount() {
 | 
				
			||||||
    playerCount.textContent = `${playerList.length} Player${playerList.length === 1 ? "" : "s"}`
 | 
					    playerCount.textContent = `${playerElements.length} Player${playerElements.length === 1 ? "" : "s"}`
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function getSocketURL() {
 | 
					function getSocketURL() {
 | 
				
			||||||
    return socketURL + (currentCode === "" ? "create" : "join?" + currentCode)
 | 
					    return socketURL + (currentCode === "" ? "create" : "join?" + currentCode)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
function getPlayerId() {
 | 
					 | 
				
			||||||
    let id = 0;
 | 
					 | 
				
			||||||
    for (let i = 0; i < playerList.length; i++) {
 | 
					 | 
				
			||||||
        const player = playerList[i];
 | 
					 | 
				
			||||||
        if (player === thisPlayer) return id;
 | 
					 | 
				
			||||||
        if (player.inGame === false) id++;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
function startGame() {
 | 
					function startGame() {
 | 
				
			||||||
    WindowManager.closeWindow("customLobby");
 | 
					    WindowManager.closeWindow("customLobby");
 | 
				
			||||||
    sendMessage("startGame");
 | 
					    sendMessage("startGame");
 | 
				
			||||||
| 
						 | 
					@ -300,7 +285,7 @@ function setActive(active) {
 | 
				
			||||||
function hideWindow() {
 | 
					function hideWindow() {
 | 
				
			||||||
    WindowManager.closeWindow("customLobby");
 | 
					    WindowManager.closeWindow("customLobby");
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, getPlayerId, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
 | 
					const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const customLobby = gameInterface
 | 
					const customLobby = gameInterface
 | 
				
			||||||
export default customLobby
 | 
					export default customLobby
 | 
				
			||||||
								
									
									
										
											15
										
									
									src/main.js
									
									
									
									
								
								
							
							
										
											15
										
									
									src/main.js
									
									
									
									
								| 
						 | 
					@ -1,16 +1,5 @@
 | 
				
			||||||
const fx_version = '0.6.7'; // FX Client Version
 | 
					const fx_version = '0.6.6.18'; // FX Client Version
 | 
				
			||||||
const fx_update = 'Feb 8'; // FX Client Last Updated
 | 
					const fx_update = 'Feb 6'; // FX Client Last Updated
 | 
				
			||||||
 | 
					 | 
				
			||||||
if ("serviceWorker" in navigator) {
 | 
					 | 
				
			||||||
  navigator.serviceWorker.addEventListener("message", (e) => {
 | 
					 | 
				
			||||||
    const message = e.data;
 | 
					 | 
				
			||||||
    if (message.event === "activate" && buildTimestamp !== message.version) {
 | 
					 | 
				
			||||||
      // worker was updated in the background
 | 
					 | 
				
			||||||
      document.getElementById("updateNotification").style.display = "block";
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  navigator.serviceWorker.register("./sw.js");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
import settingsManager from './settings.js';
 | 
					import settingsManager from './settings.js';
 | 
				
			||||||
import { clanFilter, leaderboardFilter } from "./clanFilters.js";
 | 
					import { clanFilter, leaderboardFilter } from "./clanFilters.js";
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -83,11 +83,6 @@
 | 
				
			||||||
        <h1>Donation history for </h1>
 | 
					        <h1>Donation history for </h1>
 | 
				
			||||||
        <p id="donationhistory_note">Note: donations from bots are not shown here</p>
 | 
					        <p id="donationhistory_note">Note: donations from bots are not shown here</p>
 | 
				
			||||||
        <table><tbody id="donationhistory_content"></tbody></table>
 | 
					        <table><tbody id="donationhistory_content"></tbody></table>
 | 
				
			||||||
    </div>
 | 
					 | 
				
			||||||
    <div class="window" style="display: none" id="updateNotification">
 | 
					 | 
				
			||||||
      <h3>A new version of FX is available! Reload to update</h3>
 | 
					 | 
				
			||||||
      <button onclick="window.location.reload()">Reload</button>
 | 
					 | 
				
			||||||
      <button onclick="document.getElementById('updateNotification').style.display = 'none'">Dismiss</button>
 | 
					 | 
				
			||||||
    </div></span>
 | 
					    </div></span>
 | 
				
			||||||
    <script src="variables.js?buildTimestamp"></script>
 | 
					    <script src="variables.js?buildTimestamp"></script>
 | 
				
			||||||
    <script src="fx.bundle.js?buildTimestamp"></script>
 | 
					    <script src="fx.bundle.js?buildTimestamp"></script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
								
									
									
										
											47
										
									
									static/sw.js
									
									
									
									
								
								
							
							
										
											47
										
									
									static/sw.js
									
									
									
									
								| 
						 | 
					@ -1,47 +0,0 @@
 | 
				
			||||||
const cacheName = "buildTimestamp"; // this gets replaced by the build script
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
self.addEventListener("install", (e) => {
 | 
					 | 
				
			||||||
  console.log("[Service Worker] Install");
 | 
					 | 
				
			||||||
  self.skipWaiting();
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
self.addEventListener("fetch", (e) => {
 | 
					 | 
				
			||||||
  const url = e.request.url;
 | 
					 | 
				
			||||||
  // Cache http and https only, skip unsupported chrome-extension:// and file://...
 | 
					 | 
				
			||||||
  if (!(url.startsWith('http:') || url.startsWith('https:'))) {
 | 
					 | 
				
			||||||
    return;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  e.respondWith(
 | 
					 | 
				
			||||||
    (async () => {
 | 
					 | 
				
			||||||
      const r = await caches.match(e.request);
 | 
					 | 
				
			||||||
      console.log(`[Service Worker] Fetching resource: ${url}`);
 | 
					 | 
				
			||||||
      if (r) {
 | 
					 | 
				
			||||||
        return r;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const response = await fetch(e.request);
 | 
					 | 
				
			||||||
      const cache = await caches.open(cacheName);
 | 
					 | 
				
			||||||
      console.log(`[Service Worker] Caching new resource: ${url}`);
 | 
					 | 
				
			||||||
      cache.put(e.request, response.clone());
 | 
					 | 
				
			||||||
      return response;
 | 
					 | 
				
			||||||
    })(),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
self.addEventListener("activate", (e) => {
 | 
					 | 
				
			||||||
  console.log("[Service Worker] Activated", cacheName);
 | 
					 | 
				
			||||||
  self.clients.matchAll().then(clients => {
 | 
					 | 
				
			||||||
    clients.forEach(client => client.postMessage({ event: "activate", version: cacheName }));
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
  e.waitUntil(
 | 
					 | 
				
			||||||
    caches.keys().then((keyList) => {
 | 
					 | 
				
			||||||
      return Promise.all(
 | 
					 | 
				
			||||||
        keyList.map((key) => {
 | 
					 | 
				
			||||||
          if (key === cacheName) {
 | 
					 | 
				
			||||||
            return;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return caches.delete(key);
 | 
					 | 
				
			||||||
        }),
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }),
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
		Loading…
	
		Reference in New Issue