diff --git a/README.md b/README.md index 771c9d1..d45cbf8 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Yes, this is safe. This script is open-source, and you can check the code yourse With this patch you will be able to use the latest version together with Pro. -## 👀 What features will be available? +## 💫 What features will be available? ✅ Unlimited usage time
✅ No ads
@@ -21,21 +21,25 @@ With this patch you will be able to use the latest version together with Pro. ❌ Connect phone
❌ Hotkeys (hotkey functionality breaks after patch for unknown reason) -## ❓ How to use? +## 👀 How to use? -1. **Clone the Repository (or just download source code)** - ``` bash - git clone https://github.com/k1tbyte/Wemod-patcher.git - cd Wemod-patcher - ``` -2. **Run the script** - ``` bash - bun install/npm install/yarn install/whatever you use - node ./unlocker.js - ``` -3. **Follow the instructions** +1. Go to [Releases](https://github.com/k1tbyte/Wemod-Patcher/releases) page. +2. Download latest version +3. Run and click the patch --- + +## ❓ Q&A + +- I applied the patch but when I inject I get stuck on 'Loading mods...'. + - Just close WeMod and try again +- During the game, some hacks are enabled without my input + - This is a bug after the patch, you have to turn off hotkeys in WeMod settings +- Why is the patch executable file size so large? It seems to me that you want to harm my system. + - The thing is that the application is written in Electron, so it also puts chromium, nodejs and some libraries in the exe. Maybe Electron is a temporary solution and in the future I will consider another option + +--- + ## 📜 License This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.txt) file for details. diff --git a/appicon.ico b/appicon.ico new file mode 100644 index 0000000..caf6447 Binary files /dev/null and b/appicon.ico differ diff --git a/index.html b/index.html index 7d0c8cf..12f0086 100644 --- a/index.html +++ b/index.html @@ -28,6 +28,19 @@
Waiting for action...
+ + diff --git a/index.js b/index.js index f37e483..9061c19 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,13 @@ const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const path = require('path'); const fs = require("fs"); +const Unlocker = require("./unlocker"); +const GitHubUpdater = require("./updater"); + +const packageJsonPath = path.join(__dirname, 'package.json'); +const packageData = require(packageJsonPath) +const updater = new GitHubUpdater(packageData.author, packageData.name); + function createWindow() { const win = new BrowserWindow({ @@ -20,6 +27,12 @@ function createWindow() { const checkWeModPath = (root) => fs.existsSync(path.join(root, 'WeMod.exe')) && fs.existsSync(path.join(root, 'resources/app.asar')) +function log(message, type = 'info') { + BrowserWindow.getAllWindows().forEach(win => { + win.webContents.send('log', { message, type }); + }); +} + app.whenReady().then(createWindow); app.on('window-all-closed', () => { @@ -44,6 +57,11 @@ ipcMain.handle('select-file', async () => { return null; }); +ipcMain.handle('apply-patch', async (event, path) => { + const unlocker = new Unlocker(path, (e) => log(e)) + await unlocker.start() +}) + ipcMain.handle('resolve-default-path', async () => { const defaultDir = path.join(process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local'), 'WeMod'); @@ -89,4 +107,28 @@ ipcMain.handle('start-patch', async (event, filePath) => { message: error.message }; } -}); \ No newline at end of file +}); + +ipcMain.handle("get-current-version", () => { + return packageData.version +}) + + +ipcMain.handle("check-updates", async () => { + return await updater.checkForUpdates() +}) + +ipcMain.on('open-link', () => { + require('electron').shell.openExternal("https://github.com/k1tbyte/Wemod-Patcher") +}) + +ipcMain.on("apply-update", async (event, source) => { + try { + log("Downloading update ...") + const path = await updater.downloadUpdate(source) + log("Preparation") + updater.applyUpdate(path) + } catch (err) { + log(err, "error") + } +}) \ No newline at end of file diff --git a/memoryScanner.js b/memoryScanner.js index 01ce815..0f25fed 100644 --- a/memoryScanner.js +++ b/memoryScanner.js @@ -55,6 +55,13 @@ async function patchBySignature(filePath, functionSignature, patchBytes, patchOf if (matchIndex !== -1) { const functionStartPosition = filePosition + matchIndex; + const checkBuffer = Buffer.alloc(patchBytes.length); + await fileHandle.read(checkBuffer, 0, patchBytes.length, functionStartPosition + patchOffset); + + if (Buffer.compare(checkBuffer, Buffer.from(patchBytes)) === 0) { + return 0; // Memory already patched + } + // Go to patch position await fileHandle.write(Buffer.from(patchBytes), 0, patchBytes.length, functionStartPosition + patchOffset); diff --git a/package.json b/package.json index 5088206..456a807 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,13 @@ { "name": "wemod-patcher", - "version": "1.0.0", + "version": "0.0.1", "main": "index.js", "scripts": { "start": "electron .", "build": "electron-builder --win portable" }, "keywords": [], - "author": "kitbyte", + "author": "k1tbyte", "license": "Apache-2.0", "description": "", "dependencies": { @@ -23,14 +23,22 @@ "directories": { "output": "dist" }, - "asar": true, "win": { + "icon": "./appicon.ico", "target": [{ "target": "portable", "arch": ["x64"] }], "artifactName": "${productName}.exe" }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowToChangeInstallationDirectory": true, + "installerIcon": "./appicon.ico", + "uninstallerIcon": "./appicon.ico", + "installerHeaderIcon": "./appicon.ico" + }, "compression": "maximum", "removePackageScripts": true, "removePackageKeywords": true, diff --git a/renderer.js b/renderer.js index 31b5411..5e3aed4 100644 --- a/renderer.js +++ b/renderer.js @@ -5,13 +5,22 @@ class PatcherUI { this.selectedPath = ''; this.initializeElements(); this.bindEvents(); + + ipcRenderer.on('log', (event, { message, type }) => { + this.addLog(message, type); + }); } initializeElements() { this.filePathInput = document.getElementById('file-path'); this.browseBtn = document.getElementById('browse-btn'); this.patchBtn = document.getElementById('patch-btn'); + this.updateBtn = document.getElementById('updateBtn'); + this.versionLabel = document.getElementById('version-label'); this.logSection = document.querySelector('.log-section'); + document.getElementById(`sourceBtn`).addEventListener('click', () => { + ipcRenderer.send("open-link") + }) } bindEvents() { @@ -58,8 +67,11 @@ class PatcherUI { this.addLog('Starting patch process...', 'info'); try { - const result = await ipcRenderer.invoke('start-patch', this.selectedPath); - this.addLog(result.message, result.success ? 'success' : 'error'); + await ipcRenderer.invoke( + 'apply-patch', + this.selectedPath + ); + this.addLog("Success", 'success'); } catch (error) { this.addLog('Patch failed: ' + error.message, 'error'); } finally { @@ -68,12 +80,29 @@ class PatcherUI { } resolveDefault() { + + ipcRenderer.invoke("get-current-version").then((v) => + this.versionLabel.textContent = `Current version: ${v}`); + ipcRenderer.invoke("resolve-default-path").then(path => { this.addLog(path ? 'The WeMod folder has been found!' : "WeMod folder was not found. You need to specify the path manually", path ? "success" : "warning" ); this.setPath(path) }) + + ipcRenderer.invoke("check-updates").then(result => { + if(!result) { + return; + } + + this.updateBtn.className = "" + this.updateBtn.textContent = `Update to ${result.version}` + this.updateBtn.addEventListener('click', () => { + this.updateBtn.className = "hidden" + ipcRenderer.send("apply-update", result); + }); + }) } } diff --git a/styles.css b/styles.css index f4cfe48..71d997a 100644 --- a/styles.css +++ b/styles.css @@ -15,7 +15,7 @@ body { .container { background: #21223d; - padding: 24px; + padding: 24px 24px 15px; border: 1px solid #1e2039; height: 100%; width: 100%; @@ -29,7 +29,6 @@ body { height: 100%; justify-content: center; flex-direction: column; - gap: 20px; } .header { @@ -55,6 +54,7 @@ body { .path-section { background: #26254b; padding: 16px; + margin-top: 20px; border-radius: 6px; border: 1px solid #1e2039; } @@ -97,6 +97,7 @@ body { } .patch-btn { + margin: 15px 0; background: #6d67fd; color: #f8f6f6; border: none; @@ -149,4 +150,39 @@ body { .info { color: #6d67fd; +} + +#sourceBtn { + cursor: pointer; +} + +.footer { + margin-top: 10px; + display: flex; + align-items: center; + justify-content: space-between; +} + +.footer span { + font-size: 12px; +} + +.hidden { + display: none; +} + +#updateBtn { + background: springgreen; + color: #1e2039; + font-weight: bold; + border: none; + cursor: pointer; + border-radius: 10px; + margin-left: 10px; + padding: 10px 25px; + transition: all 0.1s; +} + +#updateBtn:hover { + scale: 105% } \ No newline at end of file diff --git a/unlocker.js b/unlocker.js index 5914a83..dc01156 100644 --- a/unlocker.js +++ b/unlocker.js @@ -1,13 +1,13 @@ const path = require('path'); const fs = require('fs'); -const readline = require('readline'); const asar = require('asar'); +const { execSync } = require("child_process"); const patchBySignature = require("./memoryScanner"); const regex = /getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?}\).*?}/g; const asarPatch = "getUserAccount(){return this.#.fetch({endpoint:\"/v3/account\",method:\"GET\",name:\"/v3/account\",collectMetrics:0}).then(response=>{response.subscription={period:\"yearly\",state:\"active\"};response.flags=78;return response;})}" -const signature = "E8 ?? ?? ?? ?? 85 C0 75 ?? F6 C3 01 74 ?? 48 89 F9 E8 ?? ?? ?? ??" +const signature = "E8 ?? ?? ?? ?? ?? C0 75 ?? F6 C3 01 74 ?? 48 89 F9 E8 ?? ?? ?? ??" const patchBytes = [0x31] const patchOffset = 0x5 // ... @@ -16,157 +16,97 @@ const patchOffset = 0x5 // call near ptr funk_1445527E0 // ... -console.log("WeMod unlocker by K1tbyte") -let defaultDir = path.join(process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local'), 'WeMod'); -let appDir = null +class Unlocker { -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout -}); - -const read = (title, onSubmit, onExit) => { - rl.question(title, (answer) => { - try { - onSubmit(answer) - } finally { - rl.close() - onExit?.() - } - }); -} - -const checkWeModPaths = (dir) => { - return fs.existsSync(dir) && fs.existsSync(path.join(dir, 'WeMod.exe')) && fs.existsSync(path.join(dir, 'resources')) -} - -function getFetchFieldName(code) { - const match = code.match(/this\.#([a-zA-Z_$][0-9a-zA-Z_$]*)\.fetch/); - return match ? match[1] : null; -} - -const patchAsar = unpackedPath => { - let items = fs.readdirSync(unpackedPath,{ withFileTypes: true }) - items = items.filter(item => !item.isDirectory() && /^app-\w+/.test(item.name)); - if(items.length === 0) { - console.log(" - No app bundle found") - return; + constructor(appDir, logger) { + this.appDir = appDir; + this.logger = logger; } - let asarPatchApplied = false; - for (const item of items) { - const data = fs.readFileSync(path.join(unpackedPath, item.name), { encoding: 'utf8'}) + #getFetchFieldName(code) { + const match = code.match(/this\.#([a-zA-Z_$][0-9a-zA-Z_$]*)\.fetch/); + return match ? match[1] : null; + } - const matches = data.match(regex) - if(!matches) { - continue; - } - if(matches.length > 1) { - console.error(" - Multiple target functions found. Looks like the version is not supported") - throw new Error("Multiple target functions found"); + #patchAsar (unpackedPath) { + let items = fs.readdirSync(unpackedPath,{ withFileTypes: true }) + items = items.filter(item => !item.isDirectory() && /^app-\w+/.test(item.name)); + if(items.length === 0) { + throw new Error(" - No app bundle found"); } - const fetchFieldName = getFetchFieldName(matches[0]); - if(!fetchFieldName) { - console.error(" - Fetch field name not found") - throw new Error("Fetch field name not found"); - } + let asarPatchApplied = false; + for (const item of items) { + const data = fs.readFileSync(path.join(unpackedPath, item.name), { encoding: 'utf8'}) - const patch = asarPatch.replace(//g, fetchFieldName) - - console.log(" - Found target function in: ", item.name) - console.log(" - Patching asar...") - fs.writeFileSync(path.join(unpackedPath, item.name), data.replace(regex, patch), {encoding: 'utf8'}) - console.log(" - Patch applied") - asarPatchApplied = true; - break; - } - - if(!asarPatchApplied) { - throw new Error("Failed to apply patch"); - } -} - -const patchPE = async () => { - console.log(" - Patching PE...") - const pePath = path.join(appDir, 'WeMod.exe') - const procStart = await patchBySignature(pePath, signature, patchBytes, patchOffset) - if(procStart === -1) { - console.log(" - Signature not found or already patched") - return; - } - console.log(" - Patch saved") -} - -const start = async () => { - console.log(" - Extracting asar...") - try { - const asarPath = path.join(appDir, 'resources', 'app.asar') - asar.extractAll(asarPath, path.join(appDir, 'resources', 'app.asar.unpacked')) - } catch(e) { - console.error("Failed to extract asar", e) - return; - } - - const unpackedPath = path.join(appDir, 'resources', 'app.asar.unpacked') - - fs.renameSync(path.join(appDir, 'resources', 'app.asar'), path.join(appDir, 'resources', 'app.asar.backup')) - console.log(" - Backup saved") - patchAsar(unpackedPath) - - await asar.createPackageWithOptions(unpackedPath, path.join(appDir, 'resources', 'app.asar'), - { unpack: path.join(unpackedPath,"static/unpacked/**") }) - console.log(" - Patch saved") - - await patchPE() - console.log("Done!") - process.exit(0) -} - -const prepare = async () => { - console.log(" - Path: ", appDir || "Not found") - - if (!appDir) { - read("WeMod directory not found. Enter the path manually: ", (answer) => { - if (checkWeModPaths(answer)) { - appDir = answer; - return; + const matches = data.match(regex) + if(!matches) { + continue; + } + if(matches.length > 1) { + throw new Error(" - Multiple target functions found. Looks like the version is not supported"); } - console.log("Invalid path") - }, prepare); - return; - } - read("Continue? (y/n): ", (answer) => { - if(answer.toLowerCase() === 'y') { - start() - } - }) -} + const fetchFieldName = this.#getFetchFieldName(matches[0]); + if(!fetchFieldName) { + throw new Error(" - Fetch field name not found"); + } -if(fs.existsSync(defaultDir)) { - const items = fs.readdirSync(defaultDir, { withFileTypes: true }); - const appFolders = items - .filter(item => item.isDirectory() && /^app-\w+/.test(item.name)) - .map(item => { - const folderPath = path.join(defaultDir, item.name); - const stats = fs.statSync(folderPath); - return { - name: item.name, - path: folderPath, - mtime: stats.mtime, - }; - }); + const patch = asarPatch.replace(//g, fetchFieldName) - appFolders.sort((a, b) => b.mtime - a.mtime); - for(const folder of appFolders) { - if(checkWeModPaths(folder.path)) { - appDir = folder.path; + this.logger(" - Found target function in: " + item.name) + this.logger(" - Patching asar...") + fs.writeFileSync(path.join(unpackedPath, item.name), data.replace(regex, patch), {encoding: 'utf8'}) + this.logger(" - Patch applied") + asarPatchApplied = true; break; } + + if(!asarPatchApplied) { + throw new Error("Failed to apply patch"); + } + } + + + async #patchPE () { + this.logger(" - Patching PE...") + const pePath = path.join(this.appDir, 'WeMod.exe') + const procStart = await patchBySignature(pePath, signature, patchBytes, patchOffset) + if(procStart === -1) { + throw new Error(" - Signature not found or already patched") + } + + this.logger(procStart === 0 ? " - PE already patched" : " - Patch saved") + } + + async start () { + this.logger(" - Extracting asar...") + const asarPath = path.join(this.appDir, 'resources', 'app.asar') + const unpackedPath = path.join(this.appDir, 'resources', 'app.asar.unpacked') + const backupPath = path.join(this.appDir, 'resources', 'app.asar.backup') + + if(fs.existsSync(backupPath)) { + this.logger(" - Backup already exists") + } else { + execSync(`copy "${asarPath}" "${backupPath}"`, { encoding: "utf-8" }); + this.logger(" - Backup saved") + } + + try { + asar.extractAll(asarPath, unpackedPath) + } catch(e) { + throw new Error("Failed to extract asar: " + e) + } + + this.#patchAsar(unpackedPath) + + await asar.createPackageWithOptions(unpackedPath, asarPath, + { unpack: path.join(unpackedPath,"static/unpacked/**") }) + this.logger(" - Patch saved") + + await this.#patchPE() } } -(async () => { - await prepare(); -})(); +module.exports = Unlocker; + diff --git a/updater.js b/updater.js new file mode 100644 index 0000000..87a380f --- /dev/null +++ b/updater.js @@ -0,0 +1,105 @@ +const { app } = require("electron"); +const fs = require("fs"); +const path = require("path"); +const { exec } = require("child_process"); + +class GitHubUpdater { + constructor(owner, repo, currentVersion) { + this.owner = owner; + this.repo = repo; + this.currentVersion = currentVersion || app.getVersion(); + this.apiUrl = `https://api.github.com/repos/${owner}/${repo}/releases/latest`; + this.downloadDir = path.join(app.getPath("temp"), "update"); + } + + async checkForUpdates() { + const response = await fetch(this.apiUrl, { + headers: { "User-Agent": "GitHub-Updater" }, + }); + if (!response.ok) { + throw new Error(`An error occurred while checking for an update: ${response.statusText}`); + } + + const release = await response.json(); + const latestVersion = release.tag_name; + const assets = release.assets; + + if (this.currentVersion === latestVersion) { + return null; + } + + const asset = assets.find(a => a.name.endsWith(".exe")); + if (!asset) { + throw new Error("Unable to find files to update"); + } + + return { + version: latestVersion, + url: asset.browser_download_url, + name: asset.name, + }; + } + + async downloadUpdate(updateInfo) { + const response = await fetch(updateInfo.url); + if (!response.ok) { + throw new Error(`Error downloading file: ${response.statusText}`); + } + + if (!fs.existsSync(this.downloadDir)) { + fs.mkdirSync(this.downloadDir, { recursive: true }); + } + + const filePath = path.join(this.downloadDir, updateInfo.name); + const fileStream = fs.createWriteStream(filePath); + + await new Promise((resolve, reject) => { + const downloadStream = response.body.getReader(); + + const pump = async () => { + try { + while (true) { + const { done, value } = await downloadStream.read(); + if (done) { + fileStream.end(); + resolve(); + break; + } + fileStream.write(value); + } + } catch (error) { + reject(error); + } + }; + + fileStream.on("error", (error) => { + reject(new Error(`File write error: ${error.message}`)); + }); + + pump(); + }); + + return filePath; + } + + async applyUpdate(filePath) { + try { + const currentExecutable = process.env.PORTABLE_EXECUTABLE_FILE; + const updateScript = `Start-Sleep -Seconds 3; Copy-Item -Path '${filePath}' -Destination '${currentExecutable}' -Force; Remove-Item -Path '${filePath}' -Force; Start-Sleep -Seconds 2; Start-Process -FilePath '${currentExecutable}';`; + + exec(`start /b "" powershell -WindowStyle Hidden -Command "${updateScript}"`, { + windowsHide: true, + stdio: 'ignore' + }); + + setTimeout(() => { + app.quit(); + }, 1000); + + } catch (error) { + throw new Error(`Update failed: ${error.message}`); + } + } +} + +module.exports = GitHubUpdater;