Compare commits

...

4 Commits

Author SHA1 Message Date
peshomir ed94c65172 Mobile keybinds
- Attack percentage bar is now always redrawn after each keybind use
- UI changes: Slightly decreased the UI font size, adjusted the size of the inputs in the keybind settings and added a small margin under the keybind settings
2025-03-02 21:41:05 +02:00
peshomir 546d17c76b Improvements for modUtils.js
Added the ability to use predefined replacements for certain variables while matching and/or replacing code (name mappings)
2025-03-02 21:02:45 +02:00
peshomir 43b5b1b016 Fix for update v2.04.4 2025-02-28 09:01:37 +02:00
peshomir 3f3ad1a185 matchCode and insertCode functions 2025-02-20 22:32:06 +02:00
10 changed files with 195 additions and 27 deletions

View File

@ -56,6 +56,7 @@ 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,6 +5,8 @@ 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
@ -12,7 +14,10 @@ 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);
return output.code; let code = output.code;
if (code.endsWith(";")) code = code.slice(0, -1);
code = code.slice(5, -1); // unwrap from function
return code;
} }
class ModUtils { class ModUtils {
@ -34,6 +39,8 @@ 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 */
@ -71,11 +78,13 @@ 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;
@ -90,8 +99,14 @@ 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);
const expressionMatchResult = this.matchOne(expression); try {
return Object.fromEntries(Object.entries(groups).map(([identifier, groupNumber]) => [identifier, expressionMatchResult[groupNumber]])); const expressionMatchResult = this.matchOne(expression);
return Object.fromEntries(Object.entries(groups).map(
([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 = {};
@ -119,9 +134,38 @@ 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,23 +1,17 @@
import ModUtils from '../modUtils.js'; import ModUtils from '../modUtils.js';
// Custom lobby patches // Custom lobby patches
export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionary: dict, waitForMinification }) => { export default (/** @type {ModUtils} */ { insertCode, replaceRawCode, dictionary: dict, waitForMinification }) => {
// set player id correctly // set player id correctly
replaceCode(`function aBG(aBE) { insertCode(`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;
}`, `function aBG(aBE) { }`, `if (__fx.customLobby.isActive()) return __fx.customLobby.getPlayerId();`);
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()}",
@ -42,10 +36,10 @@ export default (/** @type {ModUtils} */ { replaceCode, replaceRawCode, dictionar
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),zs||bJ.df.show(),aN.setState(0),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),aN.setState(0),zs||bJ.df.show(),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),zs||bJ.df.show(),aN.setState(0); this.vH=0,bU.zu(),m.n.setState(0),aN.setState(0),zs||bJ.df.show();
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

@ -0,0 +1,56 @@
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,7 +1,10 @@
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,10 +1,66 @@
import { getUIGap } from "./gameInterface.js";
import { getSettings } from "./settings.js"; import { getSettings } from "./settings.js";
export const keybindFunctions = { setAbsolute: () => {}, setRelative: () => {} }; export const keybindFunctions = {
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;
if (keybindData.type === "absolute") keybindFunctions.setAbsolute(keybindData.value); executeKeybind(keybindData);
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(containerElement) { export function KeybindsInput(/** @type {HTMLElement} */ 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,6 +6,7 @@ export function KeybindsInput(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.1'; // FX Client Version const fx_version = '0.6.7.2'; // FX Client Version
const fx_update = 'Feb 15'; // FX Client Last Updated const fx_update = 'Mar 2'; // 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 } from "./keybinds.js"; import { keybindFunctions, keybindHandler, mobileKeybinds } from "./keybinds.js";
import customLobby from './customLobby.js'; import customLobby from './customLobby.js';
window.__fx = window.__fx || {}; window.__fx = window.__fx || {};
@ -33,6 +33,7 @@ __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,6 +21,7 @@ var settings = {
detailedTeamPercentage: false, detailedTeamPercentage: false,
//"customMapFileBtn": true //"customMapFileBtn": true
customBackgroundUrl: "", customBackgroundUrl: "",
keybindButtons: false,
attackPercentageKeybinds: [], attackPercentageKeybinds: [],
}; };
__fx.settings = settings; __fx.settings = settings;
@ -102,6 +103,10 @@ 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(14px + ((0.5 * (1.1vw - 0.1vh)) + 0.14rem)); font-size : calc(13px + ((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,6 +59,13 @@
margin: 0px; margin: 0px;
} }
.keybinds-input {
margin-bottom: 1em;
}
.keybinds-input input {
width: 10em;
}
.flex { .flex {
display: flex; display: flex;
} }