2024-06-07 02:08:47 -07:00
|
|
|
import beautifier from 'js-beautify';
|
|
|
|
const { js: beautify } = beautifier;
|
2024-08-18 06:33:41 -07:00
|
|
|
import UglifyJS from 'uglify-js';
|
2024-06-07 02:08:47 -07:00
|
|
|
import fs from 'fs';
|
2024-01-31 10:42:20 -08:00
|
|
|
|
|
|
|
if (!fs.existsSync("./build")) fs.mkdirSync("./build");
|
|
|
|
fs.cpSync("./static/", "./build/", { recursive: true });
|
2024-02-25 05:06:23 -08:00
|
|
|
fs.cpSync("./assets/", "./build/assets/", { recursive: true });
|
2024-03-28 09:44:06 -07:00
|
|
|
fs.cpSync("./src/fx_core.js", "./build/fx_core.js");
|
2024-02-22 01:46:53 -08:00
|
|
|
fs.writeFileSync("./build/index.html", fs.readFileSync("./build/index.html").toString().replace(/buildTimestamp/g, Date.now()));
|
2024-01-31 10:42:20 -08:00
|
|
|
let script = fs.readFileSync('./game/latest.js', { encoding: 'utf8' }).replace("\n", "").trim();
|
|
|
|
|
2024-08-18 06:33:41 -07:00
|
|
|
const exposeVarsToGlobalScope = true;
|
|
|
|
// need to first remove the iife wrapper so the top-level functions aren't inlined
|
|
|
|
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);
|
|
|
|
|
|
|
|
// for versions ^1.99.5.2
|
|
|
|
const minificationResult = UglifyJS.minify(script, {
|
|
|
|
"compress": { "arrows": false },
|
|
|
|
"mangle": false
|
|
|
|
});
|
|
|
|
if (minificationResult.error) console.log(minificationResult.error);
|
|
|
|
if (minificationResult.warnings) console.log(minificationResult.warnings);
|
|
|
|
script = minificationResult.code;
|
|
|
|
|
2024-01-31 10:42:20 -08:00
|
|
|
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
|
2024-01-31 10:42:20 -08:00
|
|
|
script = script.replace(expression, replaceValue);
|
|
|
|
return result;
|
|
|
|
}
|
2024-06-11 12:14:16 -07:00
|
|
|
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, '\\$&');
|
2024-01-31 10:42:20 -08:00
|
|
|
|
2024-02-06 09:29:40 -08:00
|
|
|
//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.");
|
2024-06-01 04:23:42 -07:00
|
|
|
const dictionary = {};
|
2024-03-26 08:38:14 -07:00
|
|
|
|
|
|
|
const matchDictionaryExpression = expression => {
|
2024-06-07 02:08:47 -07:00
|
|
|
const result = expression.exec(script);
|
2024-03-26 08:38:14 -07:00
|
|
|
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
|
2024-03-26 08:38:14 -07:00
|
|
|
// 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);
|
2024-08-26 04:31:24 -07:00
|
|
|
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);
|
2024-03-26 08:38:14 -07:00
|
|
|
return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
|
|
|
|
});
|
|
|
|
//console.log(replacementString);
|
2024-06-01 04:23:42 -07:00
|
|
|
let expressionMatchResult;
|
|
|
|
try { expressionMatchResult = replaceOne(expression, replacementString); }
|
|
|
|
catch (e) {
|
|
|
|
throw new Error("replaceRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
|
|
|
|
}
|
2024-03-26 08:38:14 -07:00
|
|
|
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]]));
|
|
|
|
}
|
2024-03-26 08:38:14 -07:00
|
|
|
const generateRegularExpression = (/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) => {
|
|
|
|
const groups = {};
|
|
|
|
let groupNumberCounter = 1;
|
2024-08-26 04:31:24 -07:00
|
|
|
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) => {
|
2024-03-26 08:38:14 -07:00
|
|
|
// if a substitution string for the "word" is specified in the nameMappings, use it
|
2024-03-28 09:44:06 -07:00
|
|
|
if (nameMappings && nameMappings.hasOwnProperty(word)) return nameMappings[word];
|
2024-03-26 08:38:14 -07:00
|
|
|
// 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;
|
2024-08-26 04:31:24 -07:00
|
|
|
// 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("
|
|
|
|
}
|
2024-03-28 09:44:06 -07:00
|
|
|
else if (groups.hasOwnProperty(word)) return "\\" + groups[word]; // regex numeric reference to the group
|
2024-03-26 08:38:14 -07:00
|
|
|
else {
|
|
|
|
groups[word] = groupNumberCounter++;
|
|
|
|
return modifier === "@" ? `(?<${word}>\\w+)` : "(\\w+)";
|
|
|
|
}
|
|
|
|
});
|
|
|
|
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
|
|
|
|
return { expression, groups };
|
|
|
|
}
|
|
|
|
|
2024-02-15 12:01:01 -08:00
|
|
|
[
|
2024-06-01 04:23:42 -07:00
|
|
|
/,this\.(?<gIsTeamGame>\w+)=this\.\w+<7\|\|9===this\.\w+,/g,
|
2024-09-09 05:58:56 -07:00
|
|
|
/=function\((\w+),(\w+),\w+\){\1===(?<game>\w+)\.(?<playerId>\w+)\?\w+\(175," "\+\w+\(\d+,\[(?<playerData>\w+)\.(?<playerNames>\w+)\[\2\]\]\)\+": ",1001,\2,\w+\(/g,
|
2024-06-01 04:23:42 -07:00
|
|
|
/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
|
2024-03-26 08:38:14 -07:00
|
|
|
].forEach(matchDictionaryExpression);
|
|
|
|
|
|
|
|
const rawCodeSegments = [
|
2024-09-09 05:58:56 -07:00
|
|
|
`aR.f1(fy)?aR.fB(fy)?a0z=__L([a0z]):(player=aR.fA(fy),oM=__L([b1.t9.xw(@playerData.@rawPlayerNames[player],b1.kx.l2(0,10),150)])+" ",a0z=(oM+=__L([b1.l5.l6(playerData.@playerBalances[player])])+" ")+(__L([b1.l5.l6(playerData.@playerTerritories[player])])+" ")+`,
|
2024-07-08 11:45:20 -07:00
|
|
|
"this.@gIsSingleplayer?this.@gLobbyMaxJoin=@SingleplayerMenu.@getSingleplayerPlayerCount():this.gLobbyMaxJoin=this.@gMaxPlayers,this.@gBots=this.gLobbyMaxJoin-this.@gHumans,this.sg=0,",
|
2024-08-26 04:31:24 -07:00
|
|
|
"[0]=__L(),@strs[1]=@game.@gIsSingleplayer?__L():__L(),",
|
2024-09-04 03:44:15 -07:00
|
|
|
"?(this.gB=Math.floor(.066*aK.fw),g5=aK.g5-4*@uiSizes.@gap-this.gB):",
|
2024-06-01 04:23:42 -07:00
|
|
|
`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);`,
|
2024-03-26 08:38:14 -07:00
|
|
|
]
|
|
|
|
|
|
|
|
rawCodeSegments.forEach(code => {
|
|
|
|
const { expression } = generateRegularExpression(code, true);
|
|
|
|
//console.log(expression);
|
|
|
|
matchDictionaryExpression(expression);
|
2024-02-15 12:01:01 -08:00
|
|
|
});
|
2024-03-26 08:38:14 -07:00
|
|
|
|
2024-03-07 05:55:59 -08:00
|
|
|
fs.writeFileSync("./build/fx_core.js", `const dictionary = ${JSON.stringify(dictionary)};\n` + fs.readFileSync("./build/fx_core.js").toString());
|
2024-01-31 10:42:20 -08:00
|
|
|
|
2024-06-11 12:14:16 -07:00
|
|
|
import applyPatches from './patches.js';
|
|
|
|
applyPatches({ replace, replaceOne, replaceRawCode, dictionary, matchOne, matchRawCode, escapeRegExp });
|
2024-02-24 10:38:37 -08:00
|
|
|
|
2024-01-31 10:42:20 -08:00
|
|
|
console.log("Formatting code...");
|
|
|
|
|
|
|
|
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
|
|
|
|
2024-01-31 10:42:20 -08:00
|
|
|
fs.writeFileSync("./build/game.js", script);
|
|
|
|
console.log("Wrote ./build/game.js");
|
|
|
|
console.log("Build done");
|