#!/usr/bin/env node // Local patch: make the multi-account limits configurable. // // Upstream hard-codes: // - one merchant per IP and per owner, // - three non-merchant characters per IP and per owner, // and only checks some of those values at login. // // This patch reads the limits from options.js in both places: // - is_player_allowed() (login, node/server_functions.js) // - enforce_limitations() (backend cron, adventure_functions.js) // // New options (defaults match upstream): // merchant_ip_limit: 1 // merchant_owner_limit: 1 // ip_limit: 3 // character_limit: 3 // // The patch is idempotent and fails loudly if an anchor is missing. const fs = require("node:fs"); const path = require("node:path"); const app = path.resolve(process.argv[2] || "/app"); const serverFunctionsFile = path.join(app, "node", "server_functions.js"); const adventureFunctionsFile = path.join(app, "adventure_functions.js"); function fail(message) { console.error("patch-multi-account-limits: " + message); process.exit(1); } function replaceOnce(source, from, to, label) { const count = source.split(from).length - 1; if (count !== 1) { fail(label + ": expected 1 anchor, found " + count); } return source.replace(from, to); } function patchServerFunctions(source) { const marker = "function get_multi_account_identity(player) {"; const oldMerchant = '\tif (player.type == "merchant") {\n' + "\t\tfor (var id in players) {\n" + "\t\t\tif ((players[id].stones && 0) || id == player.socket.id || is_free(player)) {\n" + "\t\t\t\tcontinue;\n" + "\t\t\t}\n" + '\t\t\tif (players[id].owner == player.owner && players[id].type == "merchant") {\n' + "\t\t\t\treturn false;\n" + "\t\t\t}\n" + "\t\t}\n" + "\t\treturn true;\n" + "\t}"; if (source.includes(marker)) { if (source.includes(oldMerchant)) { fail("server_functions.js has a partial merchant patch"); } console.log("patch-multi-account-limits: server_functions.js already patched"); return source; } source = replaceOnce( source, 'function get_limit_identity(player) {\n\treturn player && player.mainframe === true ? "mainframe:" + player.owner : get_ip_server(player);\n}', 'function get_limit_identity(player) {\n\treturn player && player.mainframe === true ? "mainframe:" + player.owner : get_ip_server(player);\n}\n\n' + "// Multi-account identity; mirrors the auth/temp_auth handling in enforce_limitations().\n" + "function get_multi_account_identity(player) {\n" + '\tif (player.temp_auth) return "owner:" + player.owner;\n' + '\tif (player.auth_id) return "auth:" + player.auth_id;\n' + "\treturn get_limit_identity(player);\n" + "}", "get_limit_identity", ); const newMerchant = "\t// Multi-account limits: configured in options.js; the backend cron uses the same values.\n" + '\tif (player.type == "merchant") {\n' + "\t\tvar merchant_limit = options.merchant_owner_limit === undefined ? 1 : options.merchant_owner_limit;\n" + "\t\tvar merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;\n" + "\t\tvar merchants = 0;\n" + "\t\tvar merchant_ips = 0;\n" + "\t\tvar merchant_identity = get_multi_account_identity(player);\n" + "\t\tfor (var id in players) {\n" + '\t\t\tif (id == player.socket.id || is_free(players[id]) || players[id].type != "merchant") {\n' + "\t\t\t\tcontinue;\n" + "\t\t\t}\n" + "\t\t\tif (players[id].owner == player.owner) {\n" + "\t\t\t\tmerchants++;\n" + "\t\t\t}\n" + "\t\t\tif (get_multi_account_identity(players[id]) == merchant_identity) {\n" + "\t\t\t\tmerchant_ips++;\n" + "\t\t\t}\n" + "\t\t}\n" + "\t\tif (merchants >= merchant_limit || merchant_ips >= merchant_ip_limit) {\n" + "\t\t\treturn false;\n" + "\t\t}\n" + "\t\treturn true;\n" + "\t}"; source = replaceOnce(source, oldMerchant, newMerchant, "merchant branch"); return source; } function patchAdventureFunctions(source) { const marker = "var merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;"; const oldMerchant = '\t\t\t\tif (player.type === "merchant" && (mips[player.ip] > 1 || mowners[player.owner] > 1) && to_disconnect.indexOf(player.name) === -1) {'; const oldOther = '\t\t\t\tif (player.type !== "merchant" && (ips[player.ip] > 3 * (ipx[player.ip] || 1) || owners[player.owner] > 3) && to_disconnect.indexOf(player.name) === -1) {'; if (source.includes(marker)) { if (source.includes(oldMerchant) || source.includes(oldOther)) { fail("adventure_functions.js has a partial limits patch"); } console.log("patch-multi-account-limits: adventure_functions.js already patched"); return source; } source = replaceOnce( source, "\t\t\tvar to_disconnect = [];", "\t\t\tvar to_disconnect = [];\n" + "\t\t\tvar merchant_ip_limit = options.merchant_ip_limit === undefined ? 1 : options.merchant_ip_limit;\n" + "\t\t\tvar merchant_owner_limit = options.merchant_owner_limit === undefined ? 1 : options.merchant_owner_limit;\n" + "\t\t\tvar ip_limit = options.ip_limit === undefined ? 3 : options.ip_limit;\n" + "\t\t\tvar character_limit = options.character_limit === undefined ? 3 : options.character_limit;", "limit variables", ); source = replaceOnce( source, oldMerchant, "\t\t\t\tif (\n" + '\t\t\t\t\tplayer.type === "merchant" &&\n' + "\t\t\t\t\t(mips[player.ip] > merchant_ip_limit || mowners[player.owner] > merchant_owner_limit) &&\n" + "\t\t\t\t\tto_disconnect.indexOf(player.name) === -1\n" + "\t\t\t\t) {", "merchant condition", ); source = replaceOnce( source, oldOther, "\t\t\t\tif (\n" + '\t\t\t\t\tplayer.type !== "merchant" &&\n' + "\t\t\t\t\t(ips[player.ip] > ip_limit * (ipx[player.ip] || 1) || owners[player.owner] > character_limit) &&\n" + "\t\t\t\t\tto_disconnect.indexOf(player.name) === -1\n" + "\t\t\t\t) {", "non-merchant condition", ); return source; } let serverFunctions = fs.readFileSync(serverFunctionsFile, "utf8"); serverFunctions = patchServerFunctions(serverFunctions); fs.writeFileSync(serverFunctionsFile, serverFunctions); let adventureFunctions = fs.readFileSync(adventureFunctionsFile, "utf8"); adventureFunctions = patchAdventureFunctions(adventureFunctions); fs.writeFileSync(adventureFunctionsFile, adventureFunctions); console.log("patch-multi-account-limits: wrote " + serverFunctionsFile + " and " + adventureFunctionsFile);