FXclient-mirror/build.js

137 lines
7.2 KiB
JavaScript
Raw Normal View History

2024-06-07 02:08:47 -07:00
import beautifier from 'js-beautify';
const { js: beautify } = beautifier;
import fs from 'fs';
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
fs.cpSync("./static/", "./build/", { recursive: true });
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
fs.cpSync("./src/fx_core.js", "./build/fx_core.js");
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
const replaceOne = (expression, replaceValue) => {
2024-03-27 12:40:34 -07:00
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);
2024-02-21 11:58:22 -08:00
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 => {
2024-06-07 02:08:47 -07:00
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:
2024-03-27 12:40:34 -07:00
// 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 replacementString = result.replaceAll("$", "$$").replace(/\w+/g, match => {
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]]));
}
2024-03-27 12:40:34 -07:00
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 raw = escapeRegExp(code).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", "Math"].includes(word)) return word;
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,
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175,\w+\.\w+\(18,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\),1001,\2,\w+\(/g,
/\w+\.\w+\((\w+)\)\?\w+\.\w+\(\1\)\?(\w+)=(\w+\.\w+)\(13,\[\2\]\):\(\w+=\w+\.\w+\(\1\),\2=\3\(14,\[\w+\.\w+\.\w+\((?<playerData>\w+)\.(?<rawPlayerNames>\w+)\[(\w+)\],\w+\.\w+\.\w+\(0,10\),150\),(\w+\.\w+\.\w+\()\4\.(?<playerBalances>\w+)\[\6\]\),\7\4\.(?<playerTerritories>\w+)\[\6\]\),\2\]\),\w+=!0\):\2=/g,
/function \w+\(\)\{if\(2===(?<game>\w+)\.(?<gameState>\w+)\)return 1;\w+\.\w+\(\),\1\.\2=2,\1\.\w+=\1.\w+\}/g,
2024-06-20 06:22:27 -07:00
/(function \w+\((\w+),(?<fontSize>\w+),(?<x>\w+),(?<y>\w+),(?<canvas>\w+)\){)(\6\.fillText\((?<playerData>\w+)\.(?<playerNames>\w+)\[\2\],\4,\5\)),(\2<(?<game>\w+)\.(?<gHumans>\w+)&&2!==\8\.(?<playerStates>\w+)\[[^}]+)}/g,
/\w+\.font=(?<fontGeneratorFunction>\w+\.\w+\.\w+)\(1,\.39\*this\.\w+\),/g
].forEach(matchDictionaryExpression);
const rawCodeSegments = [
2024-06-20 06:22:27 -07:00
",this.@gIsSingleplayer?this.@gLobbyMaxJoin=@SingleplayerMenu.@getSingleplayerPlayerCount():this.gLobbyMaxJoin=this.@gMaxPlayers,this.@gBots=this.gLobbyMaxJoin-this.@gHumans,this.sg=0,",
"[0]=@Translations.@txt[70],@strs[1]=@game.@gIsSingleplayer?@Translations.txt[71]:@Translations.txt[72],",
"?(this.gB=Math.floor(.0536*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):",
`for(a0L=new Array(@game.@gMaxPlayers),a0A.font=a07,@i=game.gMaxPlayers-1;0<=i;i--)a0L[i]=i+1+".",@playerData.@playerNames[i]=aY.qW.tm(playerData.@rawPlayerNames[i],a07,a0W),a0K[i]=Math.floor(a0A.measureText(playerData.playerNames[i]).width);`,
]
rawCodeSegments.forEach(code => {
const { expression } = generateRegularExpression(code, true);
//console.log(expression);
matchDictionaryExpression(expression);
});
fs.writeFileSync("./build/fx_core.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx_core.js").toString());
import applyPatches from './patches.js';
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
console.log("Formatting code...");
2024-06-07 02:08:47 -07:00
const exposeVarsToGlobalScope = true;
if (exposeVarsToGlobalScope && script.startsWith("\"use strict\"; (function () {") && script.endsWith("})();"))
script = script.slice("\"use strict\"; (function () {".length, -"})();".length);
if (exposeVarsToGlobalScope && script.startsWith("(function () {") && script.endsWith("})();"))
script = script.slice("(function () {".length, -"})();".length);
script = beautify(script, {
"indent_size": "1",
"indent_char": "\t",
"max_preserve_newlines": "5",
"preserve_newlines": true,
"keep_array_indentation": false,
"break_chained_methods": false,
"indent_scripts": "normal",
"brace_style": "collapse",
//"brace_style": "expand",
"space_before_conditional": true,
"unescape_strings": false,
"jslint_happy": false,
"end_with_newline": false,
"wrap_line_length": "250",
"indent_inner_html": false,
"comma_first": false,
"e4x": false,
"indent_empty_lines": false
});
2024-02-23 05:38:02 -08:00
fs.writeFileSync("./build/game.js", script);
console.log("Wrote ./build/game.js");
console.log("Build done");