diff --git a/build.js b/build.js index 153d64e..ba157de 100644 --- a/build.js +++ b/build.js @@ -115,7 +115,7 @@ const generateRegularExpression = (/** @type {string} */ code, /** @type {boolea // 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; + if (/^\d/.test(word) || ["return", "this", "var", "function", "new", "Math", "WebSocket"].includes(word)) return word; // for easy localizer function matching else if (word === "___localizer") { groups[word + (++localizerCount)] = groupNumberCounter++; diff --git a/patches.js b/patches.js index ccf7551..6efb570 100644 --- a/patches.js +++ b/patches.js @@ -50,14 +50,17 @@ export default ({ replace, replaceOne, replaceRawCode, dictionary, matchOne, mat o.ha(sE,2),b.h9<100?xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0):xD(0,__L([a8.jx[sE]]),3,sE,ad.gN,ad.kl,-1,!0)`); - { // Add settings button and win count - // add settings button + { // Add settings button, custom lobby button and win count + // add buttons replaceRawCode(`,new nQ("☰
"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1)]`, `,new nQ("☰
"+__L(),function(){aD6(3)},aa.ks),new nQ("",function(){at.d5(12)},aa.kg,!1), - new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5")]`) - // set settings button position + new nQ("FX Client settings", function() { __fx.WindowManager.openWindow("settings"); }, "rgba(0, 0, 20, 0.5)"), + new nQ("Join/Create custom lobby", function() { __fx.customLobby.showJoinPrompt(); }, "rgba(20, 9, 77, 0.5)")]`) + // set position replaceRawCode(`aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h);`, - `aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h); aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3);`); + `aZ.g5.vO(aD3[3].button,x+a0S+gap,a3X+h+gap,a0S,h); + aZ.g5.vO(aD3[5].button, x, a3X + h * 2 + gap * 2, a0S * 2 + gap, h / 3); + aZ.g5.vO(aD3[6].button, x, a3X + h * 2.33 + gap * 3, a0S * 2 + gap, h / 3);`); // render win count replaceRawCode(`if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ctx.imageSmoothingEnabled=!1;var iQ=a0.a4o("territorial.io"),kL=.84*aD4.gA/iQ.width;`, `if(_y.a4l(),_r.gI(),_m.gI(),aw.gI(),a0.g8()){ @@ -137,7 +140,7 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6 `, ${dict.game}.${dict.gIsTeamGame} && __fx.donationsTracker.displayHistory($2, ${rawPlayerNames}, ${gIsSingleplayer}), $1 && !isEmptySpace $3`); // Reset donation history and leaderboard filter when a new game is started - replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), "); + replaceOne(new RegExp(`,this\\.${dictionary.playerBalances}.fill\\(0\\),`, "g"), "$& __fx.donationsTracker.reset(), __fx.leaderboardFilter.reset(), __fx.customLobby.isActive() && __fx.customLobby.setActive(false), "); { // Player list and leaderboard filter tabs // Draw player list button @@ -293,6 +296,37 @@ canvas.font=aY.g0.g1(1,fontSize),canvas.fillStyle="rgba("+gR+","+tD+","+hj+",0.6 ) } + { // Custom lobbies + replaceRawCode("this.aHm=function(){i___.rX(),aM.a7U(bY.dZ.data[10].value),aM.init()}", + `this.aHm=function(){i___.rX(),aM.a7U(bY.dZ.data[10].value),aM.init()}, + __fx.customLobby.setJoinFunction(() => { i___.rX(); aM.a7U(0); aM.init(); })` + ) + replaceRawCode(`(socketId-aq.kt.a82)+"/",(socket=new WebSocket(url)`, + `(socketId-aq.kt.a82)+"/",(socket=new WebSocket(__fx.customLobby.isActive() && socketId === 1 ? __fx.customLobby.getSocketURL() : url)`) + replaceRawCode("this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)}", + "this.send=function(socketId,data){aJE(socketId),aJ4[socketId].send(data)},__fx.customLobby.setSendFunction(this.send)") + replaceRawCode("b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):", + "b7.dH(a0),0===b7.size?aq.kt.aJJ(wR,3205):__fx.customLobby.isCustomMessage(a0)||") + // set the custom lobby to inactive when clicking the "Back" button on the connection screen or leaving the lobby + replaceRawCode("this.xZ=function(){Sockets.kt.wf(3260),i___.kt.we()}", + "this.xZ=function(){Sockets.kt.wf(3260),__fx.customLobby.setActive(false),i___.kt.we()}") + replaceRawCode("this.xY=function(){this.wg(),Sockets.kt.wf(3240),aN.setState(0),i___.j(5,5)}", + `this.xY=function(){this.wg(),Sockets.kt.wf(3240),__fx.customLobby.setActive(false),aN.setState(0),i___.j(5,5)}, + __fx.customLobby.setLeaveFunction(() => this.xY())`) + /* // if this is needed again, include a check for menu state === 6 + replaceRawCode("this.wQ=function(wR,d){if(8===i.pz&&0===wR)if(4211===d)wS(d);", + "this.wQ=function(wR,d){ wR===1&&__fx.customLobby.isActive()&&__fx.customLobby.setActive(false); if(8===i.pz&&0===wR)if(4211===d)wS(d);")*/ + // if the server is unreachable + replaceRawCode("0===a7Q?g.wc(3249):", "0===a7Q?g.wc(3249):1===a7Q&&__fx.customLobby.isActive()?(g.wc(3249),__fx.customLobby.setActive(false)):") + // error descriptions + const errors = { 3249: "No servers found", 4705: "Lobby not found" }; + replaceRawCode(`i___.j(4,5,new k("⚠️ "+title,w3__,!0))`, + `i___.j(4,5,new k("⚠️ "+title, ${JSON.stringify(errors)}[w3__] || w3__,!0))`) + // map info (for the map selection menu) + replaceRawCode("this.info=new Array(Maps.totalMapCount+1),this.info[0]={name:__L(),", + "this.info=new Array(Maps.totalMapCount+1),__fx.customLobby.setMapInfo(this.info),this.info[0]={name:__L(),") + } + // Invalid hostname detection avoidance replaceRawCode(`,hostnameIsValid=0<=window.location.hostname.toLowerCase().indexOf("territorial.io"),`, `,hostnameIsValid=true,`) diff --git a/src/customLobby.js b/src/customLobby.js new file mode 100644 index 0000000..b3777a3 --- /dev/null +++ b/src/customLobby.js @@ -0,0 +1,181 @@ +import WindowManager from "./windowManager.js"; + +//const socketURL = "ws://localhost:8080/"; +const socketURL = "wss://fx.peshomir.workers.dev/"; +const customMessageMarker = 120; +let isActive = false; +let currentCode = ""; +let joinLobby = () => { }; +let leaveLobby = () => { }; +let sendRaw = (socketId, data) => { }; +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); +/*const gameInfo = { + botCount: 512 +}*/ + +const windowElement = WindowManager.create({ + name: "customLobby", + classes: "scrollable selectable flex-column text-align-center", + closable: false +}); + +const header = document.createElement("h2"); +header.textContent = "Custom Lobby"; + +const main = document.createElement("div"); +main.className = "customlobby-main"; + +const playerListContainer = document.createElement("div"); +const playerCount = document.createElement("p"); +playerCount.textContent = "0 Players"; +const playerList = document.createElement("div"); +playerListContainer.append(playerCount, playerList); + +// todo: convert the options into something similar to the automatic settings generator +const optionsContainer = document.createElement("div"); +const gameModeSelectMenu = document.createElement("select"); +setSelectMenuOptions([ + { value: 0, label: "2 Teams" }, + { value: 1, label: "3 Teams" }, + { value: 2, label: "4 Teams" }, + { value: 3, label: "5 Teams" }, + { value: 4, label: "6 Teams" }, + { value: 5, label: "7 Teams" }, + { value: 6, label: "8 Teams" }, + { value: 7, label: "Battle Royale" }, + { value: 10, label: "No Fullsend Battle Royale" }, + { value: 9, label: "Zombie mode" } +], gameModeSelectMenu); +gameModeSelectMenu.value = "7"; +const mapSelectMenu = document.createElement("select"); +function setMapInfo(maps) { + setTimeout(() => setSelectMenuOptions(maps.map((info, index) => ({ value: index.toString(), label: info.name })), mapSelectMenu), 0); +} +gameModeSelectMenu.addEventListener("change", e => sendMessage("options", ["mode", parseInt(e.target.value)])); +mapSelectMenu.addEventListener("change", e => sendMessage("options", ["map", parseInt(e.target.value)])); +/*const botCountInput = document.createElement("input"); +botCountInput.setAttribute("type", "number"); +botCountInput.setAttribute("min", "0"); +botCountInput.setAttribute("max", "512"); +botCountInput.value = "512";*/ +optionsContainer.append( + "Mode: ", gameModeSelectMenu, document.createElement("br"), + "Map: ", mapSelectMenu, document.createElement("br"), + //"Bot count: " +); + +main.append(playerListContainer, optionsContainer); + +const footer = document.createElement("footer"); +footer.style.marginTop = "10px"; +const startButton = document.createElement("button"); +const leaveButton = document.createElement("button"); +startButton.textContent = "Start game"; +leaveButton.textContent = "Leave lobby"; +startButton.addEventListener("click", startGame); +leaveButton.addEventListener("click", () => leaveLobby()); +footer.append(leaveButton, startButton); + +windowElement.append(header, main, footer); + +/** @param {HTMLSelectElement} element */ +function setSelectMenuOptions(options, element) { + options.forEach(data => { + const option = document.createElement("option"); + option.setAttribute("value", data.value); + option.textContent = data.label; + element.append(option); + }) +} + +function showJoinPrompt() { + const code = prompt("Enter a lobby code or leave blank to create a new lobby"); + if (code === null) return; + currentCode = code; + + //WindowManager.openWindow("customLobby"); + isActive = true; + joinLobby(); +} +function sendMessage(type, data) { + const message = data !== undefined ? { t: type, d: data } : { t: type } + const originalArray = textEncoder.encode(JSON.stringify(message)); + const buffer = new ArrayBuffer(originalArray.length + 1); + const view = new DataView(buffer); + // Set the first byte to the custom message marker + view.setUint8(0, customMessageMarker); + // Copy the original array starting from the second byte + const uint8ArrayView = new Uint8Array(buffer, 1); + uint8ArrayView.set(originalArray); + sendRaw(1, buffer); +} +/** @param {Uint8Array} raw */ +function isCustomMessage(raw) { + if (raw[0] !== customMessageMarker) return false; + if (raw.length === 1) return true; // ping + const subArray = new Uint8Array(raw.buffer, 1); + const message = JSON.parse(textDecoder.decode(subArray)); + const { t: type, d: data } = message; + if (type === "lobby") { + WindowManager.openWindow("customLobby"); + header.textContent = "Custom Lobby " + data.code; + gameModeSelectMenu.value = data.options.mode.toString(); + mapSelectMenu.value = data.options.map.toString(); + displayPlayers(data.players); + } else if (type === "addPlayer") addPlayer(data); + else if (type === "removePlayer") { + const index = data; + playerElements[index].remove(); + playerElements.splice(index, 1); + updatePlayerCount(); + } else if (type === "options") { + console.log(data); + const [option, value] = data; + if (option === "mode") gameModeSelectMenu.value = value.toString(); + else if (option === "map") mapSelectMenu.value = value.toString(); + } + return true; +} +/** @type {HTMLDivElement[]} */ +let playerElements = []; +/** @param {{ name: string }} player */ +function addPlayer(player) { + const div = document.createElement("div"); + div.className = "lobby-player"; + div.textContent = player.name; + playerList.append(div); + playerElements.push(div); + updatePlayerCount(); +} +/** @param {{ name: string }[]} players */ +function displayPlayers(players) { + playerElements = []; + playerList.innerHTML = ""; + players.forEach(addPlayer); + updatePlayerCount(); +} +function updatePlayerCount() { + playerCount.textContent = `${playerElements.length} Player${playerElements.length === 1 ? "" : "s"}` +} + +function getSocketURL() { + return socketURL + (currentCode === "" ? "create" : "join?" + currentCode) +} +function startGame() { + WindowManager.closeWindow("customLobby"); + sendMessage("startGame" /*{ mode: gameModeSelectMenu.value, map: mapSelectMenu.value }*/); +} + + +function setJoinFunction(f) { joinLobby = f; } +function setLeaveFunction(f) { leaveLobby = f; } +function setSendFunction(f) { sendRaw = f; } +function setActive(active) { + isActive = active; + if (active === false) WindowManager.closeWindow("customLobby"); +} +const gameInterface = { showJoinPrompt, isCustomMessage, getSocketURL, setJoinFunction, setLeaveFunction, setSendFunction, setMapInfo, isActive: () => isActive, setActive } + +const customLobby = gameInterface +export default customLobby \ No newline at end of file diff --git a/src/gameInterface.js b/src/gameInterface.js index 526b4ac..ddf8119 100644 --- a/src/gameInterface.js +++ b/src/gameInterface.js @@ -1,7 +1,7 @@ const playerDataProperties = ["playerTerritories", "playerBalances", "rawPlayerNames"]; const gameObjectProperties = ["playerId", "gIsTeamGame", "gHumans", "gLobbyMaxJoin", "gameState", "gIsSingleplayer"]; export const getVar = varName => { - if (playerDataProperties.includes(varName)) return window[dictionary.playerData][dictionary[varName]]; - if (gameObjectProperties.includes(varName)) return window[dictionary.game][dictionary[varName]]; + if (playerDataProperties.includes(varName)) return window[dictionary.playerData]?.[dictionary[varName]]; + if (gameObjectProperties.includes(varName)) return window[dictionary.game]?.[dictionary[varName]]; return window[dictionary[varName]] }; \ No newline at end of file diff --git a/src/main.js b/src/main.js index bd33681..fe11514 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,5 @@ -const fx_version = '0.6.5.6'; // FX Client Version -const fx_update = 'Oct 3'; // FX Client Last Updated +const fx_version = '0.6.6'; // FX Client Version +const fx_update = 'Oct 14'; // FX Client Last Updated import settingsManager from './settings.js'; import { clanFilter, leaderboardFilter } from "./clanFilters.js"; @@ -10,6 +10,7 @@ import playerList from "./playerList.js"; import gameScriptUtils from "./gameScriptUtils.js"; import hoveringTooltip from "./hoveringTooltip.js"; import { keybindFunctions, keybindHandler } from "./keybinds.js"; +import customLobby from './customLobby.js'; window.__fx = window.__fx || {}; const __fx = window.__fx; @@ -26,5 +27,6 @@ __fx.playerList = playerList; __fx.hoveringTooltip = hoveringTooltip; __fx.clanFilter = clanFilter; __fx.wins = winCounter; +__fx.customLobby = customLobby; console.log('Successfully loaded FX Client'); \ No newline at end of file diff --git a/src/windowManager.js b/src/windowManager.js index 7f7f17a..396e21c 100644 --- a/src/windowManager.js +++ b/src/windowManager.js @@ -1,4 +1,14 @@ var windows = {}; +const container = document.getElementById("windowContainer"); +function create(info) { + const window = document.createElement("div"); + info.element = window; + window.className = "window" + (info.classes !== undefined ? " " + info.classes : " scrollable selectable"); + window.style.display = "none"; + container.appendChild(window); + add(info); + return window; +} function add(newWindow) { windows[newWindow.name] = newWindow; windows[newWindow.name].isOpen = false; @@ -17,11 +27,11 @@ function closeWindow(windowName) { }; function closeAll() { Object.values(windows).forEach(function (windowObj) { - closeWindow(windowObj.name); + if (windowObj.closable !== false) closeWindow(windowObj.name); }); }; document.getElementById("canvasA").addEventListener("mousedown", closeAll); document.getElementById("canvasA").addEventListener("touchstart", closeAll, { passive: true }); document.addEventListener("keydown", event => { if (event.key === "Escape") closeAll(); }); -export default { add, openWindow, closeWindow, closeAll } \ No newline at end of file +export default { create, add, openWindow, closeWindow, closeAll } \ No newline at end of file diff --git a/static/index.html b/static/index.html index a088cc0..c0311e4 100644 --- a/static/index.html +++ b/static/index.html @@ -59,7 +59,7 @@ -