Moved to electron. Beta desktop app 0.0.1

This commit is contained in:
kitbyte
2025-01-04 17:53:37 +02:00
parent 5d59380361
commit 8e33e58939
10 changed files with 345 additions and 161 deletions
+17 -13
View File
@@ -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. 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 <br/> ✅ Unlimited usage time <br/>
✅ No ads <br/> ✅ No ads <br/>
@@ -21,21 +21,25 @@ With this patch you will be able to use the latest version together with Pro.
❌ Connect phone <br/> ❌ Connect phone <br/>
❌ Hotkeys (hotkey functionality breaks after patch for unknown reason) ❌ Hotkeys (hotkey functionality breaks after patch for unknown reason)
## How to use? ## 👀 How to use?
1. **Clone the Repository (or just download source code)** 1. Go to [Releases](https://github.com/k1tbyte/Wemod-Patcher/releases) page.
``` bash 2. Download latest version
git clone https://github.com/k1tbyte/Wemod-patcher.git 3. Run and click the patch
cd Wemod-patcher
```
2. **Run the script**
``` bash
bun install/npm install/yarn install/whatever you use
node ./unlocker.js
```
3. **Follow the instructions**
--- ---
## ❓ 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 ## 📜 License
This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.txt) file for details. This project is licensed under the Apache-2.0 - see the [LICENSE](LICENSE.txt) file for details.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

+13
View File
@@ -28,6 +28,19 @@
<div class="log-section"> <div class="log-section">
<div class="log-entry info">Waiting for action...</div> <div class="log-entry info">Waiting for action...</div>
</div> </div>
<footer class="footer">
<div>
<span id="version-label"></span>
<button id="updateBtn" class="hidden">
</button>
</div>
<svg id="sourceBtn" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="40" height="40" viewBox="0 0 24 24">
<path fill="#ffff" d="M10.9,2.1c-4.6,0.5-8.3,4.2-8.8,8.7c-0.5,4.7,2.2,8.9,6.3,10.5C8.7,21.4,9,21.2,9,20.8v-1.6c0,0-0.4,0.1-0.9,0.1 c-1.4,0-2-1.2-2.1-1.9c-0.1-0.4-0.3-0.7-0.6-1C5.1,16.3,5,16.3,5,16.2C5,16,5.3,16,5.4,16c0.6,0,1.1,0.7,1.3,1c0.5,0.8,1.1,1,1.4,1 c0.4,0,0.7-0.1,0.9-0.2c0.1-0.7,0.4-1.4,1-1.8c-2.3-0.5-4-1.8-4-4c0-1.1,0.5-2.2,1.2-3C7.1,8.8,7,8.3,7,7.6C7,7.2,7,6.6,7.3,6 c0,0,1.4,0,2.8,1.3C10.6,7.1,11.3,7,12,7s1.4,0.1,2,0.3C15.3,6,16.8,6,16.8,6C17,6.6,17,7.2,17,7.6c0,0.8-0.1,1.2-0.2,1.4 c0.7,0.8,1.2,1.8,1.2,3c0,2.2-1.7,3.5-4,4c0.6,0.5,1,1.4,1,2.3v2.6c0,0.3,0.3,0.6,0.7,0.5c3.7-1.5,6.3-5.1,6.3-9.3 C22,6.1,16.9,1.4,10.9,2.1z"></path>
</svg>
</footer>
</div> </div>
</div> </div>
<script src="renderer.js"></script> <script src="renderer.js"></script>
+43 -1
View File
@@ -1,6 +1,13 @@
const { app, BrowserWindow, ipcMain, dialog } = require('electron'); const { app, BrowserWindow, ipcMain, dialog } = require('electron');
const path = require('path'); const path = require('path');
const fs = require("fs"); 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() { function createWindow() {
const win = new BrowserWindow({ const win = new BrowserWindow({
@@ -20,6 +27,12 @@ function createWindow() {
const checkWeModPath = (root) => fs.existsSync(path.join(root, 'WeMod.exe')) && const checkWeModPath = (root) => fs.existsSync(path.join(root, 'WeMod.exe')) &&
fs.existsSync(path.join(root, 'resources/app.asar')) 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.whenReady().then(createWindow);
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
@@ -44,6 +57,11 @@ ipcMain.handle('select-file', async () => {
return null; 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 () => { ipcMain.handle('resolve-default-path', async () => {
const defaultDir = path.join(process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local'), 'WeMod'); 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 message: error.message
}; };
} }
}); });
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")
}
})
+7
View File
@@ -55,6 +55,13 @@ async function patchBySignature(filePath, functionSignature, patchBytes, patchOf
if (matchIndex !== -1) { if (matchIndex !== -1) {
const functionStartPosition = filePosition + matchIndex; 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 // Go to patch position
await fileHandle.write(Buffer.from(patchBytes), 0, patchBytes.length, functionStartPosition + patchOffset); await fileHandle.write(Buffer.from(patchBytes), 0, patchBytes.length, functionStartPosition + patchOffset);
+11 -3
View File
@@ -1,13 +1,13 @@
{ {
"name": "wemod-patcher", "name": "wemod-patcher",
"version": "1.0.0", "version": "0.0.1",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "electron .", "start": "electron .",
"build": "electron-builder --win portable" "build": "electron-builder --win portable"
}, },
"keywords": [], "keywords": [],
"author": "kitbyte", "author": "k1tbyte",
"license": "Apache-2.0", "license": "Apache-2.0",
"description": "", "description": "",
"dependencies": { "dependencies": {
@@ -23,14 +23,22 @@
"directories": { "directories": {
"output": "dist" "output": "dist"
}, },
"asar": true,
"win": { "win": {
"icon": "./appicon.ico",
"target": [{ "target": [{
"target": "portable", "target": "portable",
"arch": ["x64"] "arch": ["x64"]
}], }],
"artifactName": "${productName}.exe" "artifactName": "${productName}.exe"
}, },
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true,
"installerIcon": "./appicon.ico",
"uninstallerIcon": "./appicon.ico",
"installerHeaderIcon": "./appicon.ico"
},
"compression": "maximum", "compression": "maximum",
"removePackageScripts": true, "removePackageScripts": true,
"removePackageKeywords": true, "removePackageKeywords": true,
+31 -2
View File
@@ -5,13 +5,22 @@ class PatcherUI {
this.selectedPath = ''; this.selectedPath = '';
this.initializeElements(); this.initializeElements();
this.bindEvents(); this.bindEvents();
ipcRenderer.on('log', (event, { message, type }) => {
this.addLog(message, type);
});
} }
initializeElements() { initializeElements() {
this.filePathInput = document.getElementById('file-path'); this.filePathInput = document.getElementById('file-path');
this.browseBtn = document.getElementById('browse-btn'); this.browseBtn = document.getElementById('browse-btn');
this.patchBtn = document.getElementById('patch-btn'); this.patchBtn = document.getElementById('patch-btn');
this.updateBtn = document.getElementById('updateBtn');
this.versionLabel = document.getElementById('version-label');
this.logSection = document.querySelector('.log-section'); this.logSection = document.querySelector('.log-section');
document.getElementById(`sourceBtn`).addEventListener('click', () => {
ipcRenderer.send("open-link")
})
} }
bindEvents() { bindEvents() {
@@ -58,8 +67,11 @@ class PatcherUI {
this.addLog('Starting patch process...', 'info'); this.addLog('Starting patch process...', 'info');
try { try {
const result = await ipcRenderer.invoke('start-patch', this.selectedPath); await ipcRenderer.invoke(
this.addLog(result.message, result.success ? 'success' : 'error'); 'apply-patch',
this.selectedPath
);
this.addLog("Success", 'success');
} catch (error) { } catch (error) {
this.addLog('Patch failed: ' + error.message, 'error'); this.addLog('Patch failed: ' + error.message, 'error');
} finally { } finally {
@@ -68,12 +80,29 @@ class PatcherUI {
} }
resolveDefault() { resolveDefault() {
ipcRenderer.invoke("get-current-version").then((v) =>
this.versionLabel.textContent = `Current version: ${v}`);
ipcRenderer.invoke("resolve-default-path").then(path => { ipcRenderer.invoke("resolve-default-path").then(path => {
this.addLog(path ? 'The WeMod folder has been found!' : this.addLog(path ? 'The WeMod folder has been found!' :
"WeMod folder was not found. You need to specify the path manually", path ? "success" : "warning" "WeMod folder was not found. You need to specify the path manually", path ? "success" : "warning"
); );
this.setPath(path) 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);
});
})
} }
} }
+38 -2
View File
@@ -15,7 +15,7 @@ body {
.container { .container {
background: #21223d; background: #21223d;
padding: 24px; padding: 24px 24px 15px;
border: 1px solid #1e2039; border: 1px solid #1e2039;
height: 100%; height: 100%;
width: 100%; width: 100%;
@@ -29,7 +29,6 @@ body {
height: 100%; height: 100%;
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
gap: 20px;
} }
.header { .header {
@@ -55,6 +54,7 @@ body {
.path-section { .path-section {
background: #26254b; background: #26254b;
padding: 16px; padding: 16px;
margin-top: 20px;
border-radius: 6px; border-radius: 6px;
border: 1px solid #1e2039; border: 1px solid #1e2039;
} }
@@ -97,6 +97,7 @@ body {
} }
.patch-btn { .patch-btn {
margin: 15px 0;
background: #6d67fd; background: #6d67fd;
color: #f8f6f6; color: #f8f6f6;
border: none; border: none;
@@ -149,4 +150,39 @@ body {
.info { .info {
color: #6d67fd; 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%
} }
+80 -140
View File
@@ -1,13 +1,13 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); const fs = require('fs');
const readline = require('readline');
const asar = require('asar'); const asar = require('asar');
const { execSync } = require("child_process");
const patchBySignature = require("./memoryScanner"); const patchBySignature = require("./memoryScanner");
const regex = /getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?}\).*?}/g; const regex = /getUserAccount\(\)\{.*?return\s+this\.#\w+\.fetch\(\{.*?}\).*?}/g;
const asarPatch = "getUserAccount(){return this.#<fetch_field_name>.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 asarPatch = "getUserAccount(){return this.#<fetch_field_name>.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 patchBytes = [0x31]
const patchOffset = 0x5 const patchOffset = 0x5
// ... // ...
@@ -16,157 +16,97 @@ const patchOffset = 0x5
// call near ptr funk_1445527E0 // call near ptr funk_1445527E0
// ... // ...
console.log("WeMod unlocker by K1tbyte") class Unlocker {
let defaultDir = path.join(process.env.LOCALAPPDATA || path.join(process.env.HOME || process.env.USERPROFILE, 'AppData', 'Local'), 'WeMod');
let appDir = null
const rl = readline.createInterface({ constructor(appDir, logger) {
input: process.stdin, this.appDir = appDir;
output: process.stdout this.logger = logger;
});
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;
} }
let asarPatchApplied = false; #getFetchFieldName(code) {
for (const item of items) { const match = code.match(/this\.#([a-zA-Z_$][0-9a-zA-Z_$]*)\.fetch/);
const data = fs.readFileSync(path.join(unpackedPath, item.name), { encoding: 'utf8'}) return match ? match[1] : null;
}
const matches = data.match(regex) #patchAsar (unpackedPath) {
if(!matches) { let items = fs.readdirSync(unpackedPath,{ withFileTypes: true })
continue; items = items.filter(item => !item.isDirectory() && /^app-\w+/.test(item.name));
} if(items.length === 0) {
if(matches.length > 1) { throw new Error(" - No app bundle found");
console.error(" - Multiple target functions found. Looks like the version is not supported")
throw new Error("Multiple target functions found");
} }
const fetchFieldName = getFetchFieldName(matches[0]); let asarPatchApplied = false;
if(!fetchFieldName) { for (const item of items) {
console.error(" - Fetch field name not found") const data = fs.readFileSync(path.join(unpackedPath, item.name), { encoding: 'utf8'})
throw new Error("Fetch field name not found");
}
const patch = asarPatch.replace(/<fetch_field_name>/g, fetchFieldName) const matches = data.match(regex)
if(!matches) {
console.log(" - Found target function in: ", item.name) continue;
console.log(" - Patching asar...") }
fs.writeFileSync(path.join(unpackedPath, item.name), data.replace(regex, patch), {encoding: 'utf8'}) if(matches.length > 1) {
console.log(" - Patch applied") throw new Error(" - Multiple target functions found. Looks like the version is not supported");
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;
} }
console.log("Invalid path")
}, prepare);
return;
}
read("Continue? (y/n): ", (answer) => { const fetchFieldName = this.#getFetchFieldName(matches[0]);
if(answer.toLowerCase() === 'y') { if(!fetchFieldName) {
start() throw new Error(" - Fetch field name not found");
} }
})
}
if(fs.existsSync(defaultDir)) { const patch = asarPatch.replace(/<fetch_field_name>/g, fetchFieldName)
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,
};
});
appFolders.sort((a, b) => b.mtime - a.mtime); this.logger(" - Found target function in: " + item.name)
for(const folder of appFolders) { this.logger(" - Patching asar...")
if(checkWeModPaths(folder.path)) { fs.writeFileSync(path.join(unpackedPath, item.name), data.replace(regex, patch), {encoding: 'utf8'})
appDir = folder.path; this.logger(" - Patch applied")
asarPatchApplied = true;
break; 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 () => { module.exports = Unlocker;
await prepare();
})();
+105
View File
@@ -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;