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.
## 👀 What features will be available?
## 💫 What features will be available?
✅ Unlimited usage time <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/>
❌ 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.
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-entry info">Waiting for action...</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>
<script src="renderer.js"></script>
+43 -1
View File
@@ -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
};
}
});
});
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) {
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);
+11 -3
View File
@@ -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,
+31 -2
View File
@@ -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);
});
})
}
}
+38 -2
View File
@@ -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%
}
+80 -140
View File
@@ -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_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 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(/<fetch_field_name>/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(/<fetch_field_name>/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;
+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;