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 @@
+
+
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;