Compare commits

..

No commits in common. "ed94c651729b3b525a3390bfcfa4e1bf63c230a0" and "eb216fd0e4fab37b8a9a95c4eeab86c36297337f" have entirely different histories.

10 changed files with 27 additions and 195 deletions

View File

@ -56,7 +56,6 @@ script = script.replace(/\bS\[(\d+)\]/g, (_match, index) => `"${stringArray[inde
const modUtils = new ModUtils(minifyCode(script)); const modUtils = new ModUtils(minifyCode(script));
import applyPatches from './patches/main.js'; import applyPatches from './patches/main.js';
console.log("Applying patches...");
applyPatches(modUtils); applyPatches(modUtils);
// for versions ^1.99.5.2 // for versions ^1.99.5.2

View File

@ -5,8 +5,6 @@ import UglifyJS from 'uglify-js';
const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); const escapeRegExp = (/** @type {string} */ string) => string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
export function minifyCode(/** @type {string} */ script) { export function minifyCode(/** @type {string} */ script) {
// "return" statements outside of a function throw a parse error
script = "()=>{" + script + "}";
const output = UglifyJS.minify(script, { const output = UglifyJS.minify(script, {
compress: false, compress: false,
mangle: false mangle: false
@ -14,10 +12,7 @@ export function minifyCode(/** @type {string} */ script) {
if (output.error) throw output.error; if (output.error) throw output.error;
if (output.warnings) throw (output.warnings); if (output.warnings) throw (output.warnings);
if (output.warnings) console.warn(output.warnings); if (output.warnings) console.warn(output.warnings);
let code = output.code; return output.code;
if (code.endsWith(";")) code = code.slice(0, -1);
code = code.slice(5, -1); // unwrap from function
return code;
} }
class ModUtils { class ModUtils {
@ -39,8 +34,6 @@ class ModUtils {
this.matchRawCode = this.matchRawCode.bind(this); this.matchRawCode = this.matchRawCode.bind(this);
this.replaceCode = this.replaceCode.bind(this); this.replaceCode = this.replaceCode.bind(this);
this.waitForMinification = this.waitForMinification.bind(this); this.waitForMinification = this.waitForMinification.bind(this);
this.matchCode = this.matchCode.bind(this);
this.insertCode = this.insertCode.bind(this);
} }
/** @param {RegExp} expression */ /** @param {RegExp} expression */
@ -78,13 +71,11 @@ class ModUtils {
// Return value example: // Return value example:
// When replaceRawCode or matchRawCode are called with "var1=var2+1;" as the code // 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" } // and this matches "a=b+1;", the returned value will be the object: { var1: "a", var2: "b" }
/** @param {{ [x: string]: string; }} [nameMappings] */
replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) { replaceRawCode(/** @type {string} */ raw, /** @type {string} */ result, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings); const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
let localizerCount = 0; let localizerCount = 0;
let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,") let replacementString = result.replaceAll("$", "$$").replaceAll("__L()", "__L)").replaceAll("__L(", "__L,")
.replace(/\w+/g, match => { .replace(/\w+/g, match => {
if (nameMappings !== undefined && nameMappings.hasOwnProperty(match)) return nameMappings[match];
// these would get stored as "___localizer1", "___localizer2", ... // these would get stored as "___localizer1", "___localizer2", ...
if (match === "__L") match = "___localizer" + (++localizerCount); if (match === "__L") match = "___localizer" + (++localizerCount);
return groups.hasOwnProperty(match) ? "$" + groups[match] : match; return groups.hasOwnProperty(match) ? "$" + groups[match] : match;
@ -99,14 +90,8 @@ class ModUtils {
} }
matchRawCode(/** @type {string} */ raw, nameMappings) { matchRawCode(/** @type {string} */ raw, nameMappings) {
const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings); const { expression, groups } = this.generateRegularExpression(raw, false, nameMappings);
try {
const expressionMatchResult = this.matchOne(expression); const expressionMatchResult = this.matchOne(expression);
return Object.fromEntries(Object.entries(groups).map( return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]));
([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]]
));
} catch (e) {
throw new Error("matchRawCode match error:\n\n" + e + "\n\nRaw code: " + raw + "\n");
}
} }
generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) { generateRegularExpression(/** @type {string} */ code, /** @type {boolean} */ isForDictionary, nameMappings) {
const groups = {}; const groups = {};
@ -134,38 +119,9 @@ class ModUtils {
let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g"); let expression = new RegExp(isForDictionary ? raw.replaceAll("@@", "@") : raw, "g");
return { expression, groups }; return { expression, groups };
} }
replaceCode(code, replacement, options) {
/** return this.replaceRawCode(minifyCode(code), replacement);
* @typedef {{ dictionary?: { [x: string]: string } }} BaseOptions
* @typedef {BaseOptions & { addToDictionary?: string[] }} MatchCodeOptions
*/
matchCode(code, /** @type {MatchCodeOptions=} */ options) {
const result = this.matchRawCode(minifyCode(code));
if (options?.addToDictionary !== undefined) {
options.addToDictionary.forEach(varName => {
if (result[varName] === undefined)
throw new Error(`matchCode addToDictionary error: ${varName} was undefined in the match results`)
this.addToDictionary(varName, result[varName]);
});
} }
return result;
}
replaceCode(/** @type {string} */ code, /** @type {string} */ replacement, /** @type {BaseOptions=} */ options) {
return this.replaceRawCode(minifyCode(code), replacement, options?.dictionary);
}
/**
* @param {string} code
* @param {string} codeToInsert
* @param {BaseOptions} [options]
*/
insertCode(code, codeToInsert, options) {
const insertionPoint = "/* here */";
if (!code.includes(insertionPoint)) throw new Error("insertCode: No insertion point found");
return this.replaceCode(code.replace(insertionPoint, ""), code.replace(insertionPoint, codeToInsert), options);
}
waitForMinification(/** @type {Function} */ handler) { waitForMinification(/** @type {Function} */ handler) {
this.postMinifyHandlers.push(handler); this.postMinifyHandlers.push(handler);
} }

View File

@ -1,17 +1,23 @@
import ModUtils from '../modUtils.js'; import ModUtils from '../modUtils.js';
// Custom lobby patches // Custom lobby patches
export default (/** @type {ModUtils} */ { insertCode, replaceRawCode, dictionary: dict, waitForMinification }) => { export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionary: dict, waitForMinification }) => {
// set player id correctly // set player id correctly
insertCode(`function aBG(aBE) { replaceCode(`function aBG(aBE) {
if (!Lobby.aAl) { return -1; } if (!Lobby.aAl) { return -1; }
/* here */
var s = aBE.length; var s = aBE.length;
var qu = Lobby.aAl.qu; var qu = Lobby.aAl.qu;
for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } } for (var i = 0; i < s; i++) { if (aBE[i].qu === qu) { return i; } }
return -1; return -1;
}`, `if (__fx.customLobby.isActive()) return __fx.customLobby.getPlayerId();`); }`, `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(() => { waitForMinification(() => {
replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}", replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(0),aM.init()}",
@ -36,10 +42,10 @@ export default (/** @type {ModUtils} */ { insertCode, replaceRawCode, dictionary
wR===1 && __fx.customLobby.isActive() && ${dict.MenuManager}.${dict.getState}() !== 6 && __fx.customLobby.setActive(false); 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);`) if(8===i.pz&&0===wR)if(4211===d)wS(d);`)
// when leaving a game // 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),aN.setState(0),zs||bJ.df.show(),2===this.a3D?i.ky.a3U():1===this.a3D?i.j(19):i.j(5,5)}", 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()), `this.wl=function(zs){a1.gZ||az.oO.a11.length||(az.oO.a11=az.a12.vd()),
__fx.customLobby.isActive() === false && ap.ky.zt(), __fx.customLobby.isActive() === false && ap.ky.zt(),
this.vH=0,bU.zu(),m.n.setState(0),aN.setState(0),zs||bJ.df.show(); 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)}`) 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 // do not display lobby UI
replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`, replaceRawCode(`(sV.style.backdropFilter="blur(4px)",sV.style.webkitBackdropFilter="blur(4px)"),`,

View File

@ -1,56 +0,0 @@
export default (/** @type {import('../modUtils.js').default} */ { insertCode, replaceCode, matchCode }) => {
const { mainCanvas, x, y } = insertCode(`this.te = function() {
if (!this.b()) { return; }
mainCanvas.drawImage(canvas, x, this.y);
/* here */
}`, `if (__fx.settings.keybindButtons) __fx.mobileKeybinds.draw(mainCanvas, x, this.y);`)
const { h, redraw } = insertCode(`a6k = Math.floor(3 * this.h / 2);
a4M = c.pZ.rN(1, Math.floor(0.5 * this.h));
canvas = document.createElement("canvas");
canvas.width = w;
/* here */
canvas.height = this.h;
ctx = canvas.getContext("2d", { alpha: true });
ctx.font = a4M;
c.pZ.textBaseline(ctx, 1);
c.pZ.textAlign(ctx, 1);
this.a6m();
redraw();
`, `__fx.mobileKeybinds.setSize(w, this.h, mainCanvas)`, { dictionary: { mainCanvas } })
const { ba, gap } = matchCode(`this.h = Math.floor(0.066 * h___.pb); w = h___.w - 4 * ba.gap - this.h;`);
const { bd, requestRepaint } = insertCode(`this.gm = function(kt, ku) {
if (!this.b()) { return false; }
/* here */
if (!a.a0n(kt, ku)) { return false; }
aR.mC = false;
if (a6w(this, kt, ku)) { bd.requestRepaint = true; }
return true;
};`,
`if (__fx.settings.keybindButtons && ku > this.y - Math.floor(ba.gap / 4) - this.h && ku < this.y - Math.floor(ba.gap / 4) && __fx.mobileKeybinds.click(kt - x)) return true;`,
{ dictionary: { x, y, h, ba, gap } }
)
insertCode(
`var a6l = 11 / 12; /* here */`,
`__fx.keybindFunctions.repaintAttackPercentageBar = function() { redraw(); bd.requestRepaint = true; };`,
{ dictionary: { redraw, bd, requestRepaint } }
)
// fix to correctly display peace vote menu and game messages (prevent overlap with keybind buttons)
replaceCode(`if (a.a4y(aM.a4u())) {
if (au.b) { return a.y - a.h - 2 * a4a; }
else { return a.y - a4a; }
}`, `if (a.a4y(aM.a4u())) {
if (au.b) { return __fx.settings.keybindButtons ? a.y - 2 * a.h - 3 * a4a : a.y - a.h - 2 * a4a; }
else { return __fx.settings.keybindButtons ? a.y - a.h - 2 * a4a : a.y - a4a; }
}`)
insertCode(
`if (a.a4y(aM.a4u())) { return /* here */ a.y - h - ba.gap; }`,
`__fx.settings.keybindButtons ? a.y - 2 * (h + ba.gap) : `
)
}

View File

@ -1,10 +1,7 @@
const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"]; const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"];
const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"]; const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"];
export const getVar = varName => { export const getVar = varName => {
if (playerDataProperties.includes(varName)) return window[dictionary.playerData]?.[dictionary[varName]]; if (playerDataProperties.includes(varName)) return window[dictionary.playerData]?.[dictionary[varName]];
if (gameObjectProperties.includes(varName)) return window[dictionary.game]?.[dictionary[varName]]; if (gameObjectProperties.includes(varName)) return window[dictionary.game]?.[dictionary[varName]];
return window[dictionary[varName]] return window[dictionary[varName]]
}; };
export const getUIGap = () => Math.floor(window[dictionary.uiSizes]?.[dictionary.gap] ?? 10);

View File

@ -1,66 +1,10 @@
import { getUIGap } from "./gameInterface.js";
import { getSettings } from "./settings.js"; import { getSettings } from "./settings.js";
export const keybindFunctions = { export const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} };
setAbsolute: () => {},
setRelative: () => {},
repaintAttackPercentageBar: () => {}
};
export const keybindHandler = key => { export const keybindHandler = key => {
const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key); const keybindData = getSettings().attackPercentageKeybinds.find(keybind => keybind.key === key);
if (keybindData === undefined) return false; if (keybindData === undefined) return false;
executeKeybind(keybindData); if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value);
else keybindFunctions.setRelative(keybindData.value);
return true; return true;
}; };
function executeKeybind(keybind) {
if (keybind.type === "absolute") keybindFunctions.setAbsolute(keybind.value);
else keybindFunctions.setRelative(keybind.value);
keybindFunctions.repaintAttackPercentageBar();
}
// mobile keybinds (keybind buttons)
let canvas;
let width = 0;
let height = 0;
const maxCount = 6;
export const mobileKeybinds = {
setSize: (w, h, mainCanvas) => {
if (getSettings().keybindButtons !== true) return;
width = w;
height = h;
// redraw
canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
const fontName = mainCanvas.font.split("px ", 2)[1];
ctx.font = "bold " + h / 2 + "px " + fontName;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const keybinds = getSettings().attackPercentageKeybinds.slice(0, maxCount);
const gap = getUIGap() / 4;
const buttonWidth = (w - gap * (maxCount - 1)) / maxCount;
keybinds.forEach((keybind, i) => {
ctx.fillStyle = "rgba(0, 0, 0, 0.8)";
ctx.fillRect(i * (buttonWidth + gap), 0, buttonWidth, h);
ctx.fillStyle = "white";
const label = keybind.type === "absolute" ? (keybind.value * 100).toFixed() + "%" : "x " + Math.round(keybind.value * 100) / 100;
ctx.fillText(label, (i + 0.5) * (buttonWidth + gap), h / 2);
});
},
click: (xRelative) => {
if (xRelative < 0 || xRelative > width) return false;
const keybinds = getSettings().attackPercentageKeybinds;
const index = Math.floor(xRelative / width * maxCount);
if (index >= keybinds.length) return false;
executeKeybind(keybinds[index]);
return true;
},
draw: (mainCanvas, x, y) => {
mainCanvas.drawImage(canvas, x, y - (height + getUIGap() / 4));
}
}

View File

@ -1,4 +1,4 @@
export function KeybindsInput(/** @type {HTMLElement} */ containerElement) { export function KeybindsInput(containerElement) {
const header = document.createElement("p"); const header = document.createElement("p");
header.innerText = "Attack Percentage Keybinds"; header.innerText = "Attack Percentage Keybinds";
const keybindContainer = document.createElement("div"); const keybindContainer = document.createElement("div");
@ -6,7 +6,6 @@ export function KeybindsInput(/** @type {HTMLElement} */ containerElement) {
const keybindAddButton = document.createElement("button"); const keybindAddButton = document.createElement("button");
keybindAddButton.innerText = "Add"; keybindAddButton.innerText = "Add";
containerElement.append(header, keybindContainer, keybindAddButton); containerElement.append(header, keybindContainer, keybindAddButton);
containerElement.className = "keybinds-input";
this.container = keybindContainer; this.container = keybindContainer;
this.keys = [ "key", "type", "value" ]; this.keys = [ "key", "type", "value" ];
this.objectArray = []; this.objectArray = [];

View File

@ -1,5 +1,5 @@
const fx_version = '0.6.7.2'; // FX Client Version const fx_version = '0.6.7.1'; // FX Client Version
const fx_update = 'Mar 2'; // FX Client Last Updated const fx_update = 'Feb 15'; // FX Client Last Updated
if ("serviceWorker" in navigator) { if ("serviceWorker" in navigator) {
navigator.serviceWorker.addEventListener("message", (e) => { navigator.serviceWorker.addEventListener("message", (e) => {
@ -20,7 +20,7 @@ import winCounter from "./winCounter.js";
import playerList from "./playerList.js"; import playerList from "./playerList.js";
import gameScriptUtils from "./gameScriptUtils.js"; import gameScriptUtils from "./gameScriptUtils.js";
import hoveringTooltip from "./hoveringTooltip.js"; import hoveringTooltip from "./hoveringTooltip.js";
import { keybindFunctions, keybindHandler, mobileKeybinds } from "./keybinds.js"; import { keybindFunctions, keybindHandler } from "./keybinds.js";
import customLobby from './customLobby.js'; import customLobby from './customLobby.js';
window.__fx = window.__fx || {}; window.__fx = window.__fx || {};
@ -33,7 +33,6 @@ __fx.utils = gameScriptUtils;
__fx.WindowManager = WindowManager; __fx.WindowManager = WindowManager;
__fx.keybindFunctions = keybindFunctions; __fx.keybindFunctions = keybindFunctions;
__fx.keybindHandler = keybindHandler; __fx.keybindHandler = keybindHandler;
__fx.mobileKeybinds = mobileKeybinds;
__fx.donationsTracker = donationsTracker; __fx.donationsTracker = donationsTracker;
__fx.playerList = playerList; __fx.playerList = playerList;
__fx.hoveringTooltip = hoveringTooltip; __fx.hoveringTooltip = hoveringTooltip;

View File

@ -21,7 +21,6 @@ var settings = {
detailedTeamPercentage: false, detailedTeamPercentage: false,
//"customMapFileBtn": true //"customMapFileBtn": true
customBackgroundUrl: "", customBackgroundUrl: "",
keybindButtons: false,
attackPercentageKeybinds: [], attackPercentageKeybinds: [],
}; };
__fx.settings = settings; __fx.settings = settings;
@ -103,10 +102,6 @@ const settingsManager = new (function () {
"A custom image to be shown as the main menu background instead of the currently selected map.", "A custom image to be shown as the main menu background instead of the currently selected map.",
}, },
KeybindsInput, KeybindsInput,
{
for: "keybindButtons", type: "checkbox",
label: "Keybind buttons", note: "Show keybind buttons above the troop selector (max 6)"
}
]; ];
const settingsContainer = document.querySelector(".settings .scrollable"); const settingsContainer = document.querySelector(".settings .scrollable");
var inputFields = {}; // (includes select menus) var inputFields = {}; // (includes select menus)

View File

@ -28,7 +28,7 @@
border-width : 2px; border-width : 2px;
border-width : calc(0.15 * (1vw + 1vh)); border-width : calc(0.15 * (1vw + 1vh));
font-size : 20px; font-size : 20px;
font-size : calc(13px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem)); font-size : calc(14px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem));
max-height : 90%; max-height : 90%;
transition : 0.2s; transition : 0.2s;
z-index : 10; z-index : 10;
@ -59,13 +59,6 @@
margin: 0px; margin: 0px;
} }
.keybinds-input {
margin-bottom: 1em;
}
.keybinds-input input {
width: 10em;
}
.flex { .flex {
display: flex; display: flex;
} }