New code replacement algorithm; fix custom lobby issue
parent
162bd36f29
commit
2688e86b9d
127
build.js
127
build.js
|
@ -1,17 +1,21 @@
|
|||
// @ts-check
|
||||
import beautifier from 'js-beautify';
|
||||
const { js: beautify } = beautifier;
|
||||
import UglifyJS from 'uglify-js';
|
||||
import fs from 'fs';
|
||||
import webpack from 'webpack';
|
||||
import path from 'path';
|
||||
import applyPatches from './patches.js';
|
||||
import applyPatches from './patches/patches.js';
|
||||
import ModUtils, { minifyCode } from './modUtils.js';
|
||||
|
||||
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
|
||||
fs.cpSync("./static/", "./build/", { recursive: true });
|
||||
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
|
||||
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
|
||||
const buildTimestamp = Date.now().toString();
|
||||
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 = () => new Promise((resolve, reject) => {
|
||||
const buildClientCode = () => /** @type {Promise<void>} */(new Promise((resolve, reject) => {
|
||||
webpack({
|
||||
mode: 'production',
|
||||
entry: { fxClient: "./src/main.js" },
|
||||
|
@ -32,9 +36,9 @@ const buildClientCode = () => new Promise((resolve, reject) => {
|
|||
}
|
||||
else resolve();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
|
||||
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).trim();
|
||||
|
||||
const exposeVarsToGlobalScope = true;
|
||||
// need to first remove the iife wrapper so the top-level functions aren't inlined
|
||||
|
@ -50,93 +54,28 @@ if (stringArrayRaw === undefined) throw new Error("cannot find the string array"
|
|||
const stringArray = JSON.parse(stringArrayRaw);
|
||||
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
|
||||
const minificationResult = UglifyJS.minify(script, {
|
||||
const minificationResult = UglifyJS.minify(modUtils.script, {
|
||||
"compress": { "arrows": false },
|
||||
"mangle": false
|
||||
});
|
||||
if (minificationResult.error) console.log(minificationResult.error);
|
||||
if (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);
|
||||
script = minificationResult.code;
|
||||
modUtils.script = minificationResult.code;
|
||||
|
||||
const replaceOne = (expression, replaceValue) => {
|
||||
const result = matchOne(expression);
|
||||
// this (below) works correctly because expression.lastIndex gets reset above in matchOne when there is no match
|
||||
script = script.replace(expression, replaceValue);
|
||||
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 };
|
||||
}
|
||||
const {
|
||||
matchDictionaryExpression,
|
||||
generateRegularExpression
|
||||
} = modUtils;
|
||||
const dictionary = modUtils.dictionary;
|
||||
|
||||
[
|
||||
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
|
||||
|
@ -161,18 +100,24 @@ rawCodeSegments.forEach(code => {
|
|||
matchDictionaryExpression(expression);
|
||||
});
|
||||
|
||||
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
|
||||
modUtils.executePostMinifyHandlers();
|
||||
applyPatches(modUtils);
|
||||
script = modUtils.script;
|
||||
|
||||
await buildClientCode();
|
||||
// the dictionary should maybe get embedded into one of the files in the bundle
|
||||
fs.writeFileSync("./build/fx.bundle.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx.bundle.js").toString());
|
||||
fs.writeFileSync(
|
||||
"./build/fx.bundle.js",
|
||||
`const buildTimestamp = "${buildTimestamp}"; const dictionary = ${JSON.stringify(dictionary)};\n`
|
||||
+ fs.readFileSync("./build/fx.bundle.js").toString()
|
||||
);
|
||||
|
||||
console.log("Formatting code...");
|
||||
|
||||
script = beautify(script, {
|
||||
"indent_size": "1",
|
||||
"indent_size": 1,
|
||||
"indent_char": "\t",
|
||||
"max_preserve_newlines": "5",
|
||||
"max_preserve_newlines": 5,
|
||||
"preserve_newlines": true,
|
||||
"keep_array_indentation": false,
|
||||
"break_chained_methods": false,
|
||||
|
@ -183,7 +128,7 @@ script = beautify(script, {
|
|||
"unescape_strings": false,
|
||||
"jslint_happy": false,
|
||||
"end_with_newline": false,
|
||||
"wrap_line_length": "250",
|
||||
"wrap_line_length": 250,
|
||||
"indent_inner_html": false,
|
||||
"comma_first": false,
|
||||
"e4x": false,
|
||||
|
|
|
@ -0,0 +1,134 @@
|
|||
// @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;
|
|
@ -0,0 +1,81 @@
|
|||
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,`)
|
||||
});
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import assets from './assets.js';
|
||||
export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
|
||||
import assets from '../assets.js';
|
||||
import ModUtils from '../modUtils.js';
|
||||
export default (/** @type {ModUtils} */ { replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp }) => {
|
||||
|
||||
// Constants for easy usage of otherwise long variable access expressions
|
||||
const dict = dictionary;
|
||||
|
@ -302,66 +303,6 @@ 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
|
||||
replaceRawCode(`qr=Math.floor(100*f0+.5)+"%"`,
|
||||
`qr = (__fx.settings.detailedTeamPercentage ? (100*f0).toFixed(2) : Math.floor(100*f0+.5)) + "%"`)
|
|
@ -30,8 +30,8 @@ main.className = "customlobby-main";
|
|||
const playerListContainer = document.createElement("div");
|
||||
const playerCount = document.createElement("p");
|
||||
playerCount.textContent = "0 Players";
|
||||
const playerList = document.createElement("div");
|
||||
playerListContainer.append(playerCount, playerList);
|
||||
const playerListDiv = document.createElement("div");
|
||||
playerListContainer.append(playerCount, playerListDiv);
|
||||
|
||||
const optionsContainer = document.createElement("div");
|
||||
optionsContainer.className = "text-align-left";
|
||||
|
@ -195,40 +195,46 @@ function isCustomMessage(raw) {
|
|||
Object.entries(data.options).forEach(([option, value]) => updateOption(option, value));
|
||||
displayPlayers(data.players);
|
||||
} else if (type === "addPlayer") {
|
||||
addPlayer(data);
|
||||
addPlayer({ name: data.name, inGame: false, isHost: false });
|
||||
updatePlayerCount();
|
||||
} else if (type === "removePlayer") {
|
||||
const index = data;
|
||||
playerElements[index].element.remove();
|
||||
playerElements.splice(index, 1);
|
||||
playerList[index].element.remove();
|
||||
playerList.splice(index, 1);
|
||||
updatePlayerCount();
|
||||
} else if (type === "inLobby") {
|
||||
const index = data;
|
||||
playerElements[index].inGameBadge.className = "d-none";
|
||||
playerList[index].inGameBadge.className = "d-none";
|
||||
} else if (type === "options") {
|
||||
const [option, value] = data;
|
||||
updateOption(option, value);
|
||||
} else if (type === "setHost") {
|
||||
const index = data;
|
||||
playerElements[index].isHost = true;
|
||||
playerElements[index].hostBadge.className = "";
|
||||
playerList[index].isHost = true;
|
||||
playerList[index].hostBadge.className = "";
|
||||
} else if (type === "host") {
|
||||
playerIsHost = true;
|
||||
startButton.disabled = false;
|
||||
optionsContainer.classList.remove("disabled");
|
||||
playerElements.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
|
||||
playerList.forEach(p => { if (!p.isHost) p.kickButton.className = "" });
|
||||
} else if (type === "serverMessage") alert(data);
|
||||
return true;
|
||||
}
|
||||
/** @type {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean }[]} */
|
||||
let playerElements = [];
|
||||
|
||||
/** @typedef {{ element: HTMLDivElement, hostBadge: HTMLSpanElement, inGameBadge: HTMLSpanElement, kickButton: HTMLButtonElement, isHost: boolean, inGame: boolean }} PlayerListEntry */
|
||||
|
||||
/** @type {PlayerListEntry[]} */
|
||||
let playerList = [];
|
||||
/** @type {PlayerListEntry} */
|
||||
let thisPlayer;
|
||||
|
||||
function createBadge(text, visible) {
|
||||
const badge = document.createElement("span");
|
||||
badge.textContent = text;
|
||||
badge.className = visible ? "" : "d-none";
|
||||
return badge;
|
||||
}
|
||||
/** @typedef {{ name: string, isHost?: boolean, inGame?: boolean }} PlayerInfo */
|
||||
/** @typedef {{ name: string, isHost: boolean, inGame: boolean }} PlayerInfo */
|
||||
/** @param {PlayerInfo} player */
|
||||
function addPlayer(player) {
|
||||
const div = document.createElement("div");
|
||||
|
@ -241,13 +247,13 @@ function addPlayer(player) {
|
|||
const hostBadge = createBadge("Host", player.isHost);
|
||||
const inGameBadge = createBadge("In Game", player.inGame);
|
||||
div.append(hostBadge, inGameBadge, kickButton);
|
||||
playerList.append(div);
|
||||
playerElements.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost });
|
||||
playerListDiv.append(div);
|
||||
playerList.push({ element: div, hostBadge, inGameBadge, kickButton, isHost: player.isHost, inGame: player.inGame });
|
||||
}
|
||||
function kickButtonHandler(event) {
|
||||
const button = event.target;
|
||||
for (let index = 0; index < playerElements.length; index++) {
|
||||
if (playerElements[index].kickButton === button) {
|
||||
for (let index = 0; index < playerList.length; index++) {
|
||||
if (playerList[index].kickButton === button) {
|
||||
sendMessage("kick", index);
|
||||
break;
|
||||
}
|
||||
|
@ -255,18 +261,27 @@ function kickButtonHandler(event) {
|
|||
}
|
||||
/** @param {PlayerInfo[]} players */
|
||||
function displayPlayers(players) {
|
||||
playerElements = [];
|
||||
playerList.innerHTML = "";
|
||||
playerList = [];
|
||||
playerListDiv.innerHTML = "";
|
||||
players.forEach(addPlayer);
|
||||
thisPlayer = playerList[playerList.length - 1];
|
||||
updatePlayerCount();
|
||||
}
|
||||
function updatePlayerCount() {
|
||||
playerCount.textContent = `${playerElements.length} Player${playerElements.length === 1 ? "" : "s"}`
|
||||
playerCount.textContent = `${playerList.length} Player${playerList.length === 1 ? "" : "s"}`
|
||||
}
|
||||
|
||||
function getSocketURL() {
|
||||
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() {
|
||||
WindowManager.closeWindow("customLobby");
|
||||
sendMessage("startGame");
|
||||
|
@ -285,7 +300,7 @@ function setActive(active) {
|
|||
function hideWindow() {
|
||||
WindowManager.closeWindow("customLobby");
|
||||
}
|
||||
const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
|
||||
const gameInterface = { gameInfo: optionsValues, showJoinPrompt, isCustomMessage, getSocketURL, getPlayerId, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, rejoinLobby, hideWindow, isActive: () => isActive, setActive }
|
||||
|
||||
const customLobby = gameInterface
|
||||
export default customLobby
|
Loading…
Reference in New Issue