From f11e84efd7127a575131d88bef3209060ea98f3c Mon Sep 17 00:00:00 2001 From: HorizonCode Date: Sat, 13 Jan 2024 23:41:40 +0100 Subject: [PATCH] added launching, patching, and ui override --- main.js | 166 +++++++++++++++++++++- package-lock.json | 93 +++++++++++- package.json | 2 + preload.js | 4 +- src/App.svelte | 2 +- src/pages/Launch.svelte | 2 +- src/pages/Settings.svelte | 7 +- src/util/executeUtil.js | 20 +++ src/util/formattingUtil.js | 13 ++ src/util/mathUtil.ts | 2 +- src/util/osuUtil.js | 280 ++++++++++++++++++++++++++++++++++++- tests/fileHash.js | 14 ++ tests/osuConfig.js | 9 ++ 13 files changed, 599 insertions(+), 15 deletions(-) create mode 100644 src/util/executeUtil.js create mode 100644 src/util/formattingUtil.js create mode 100644 tests/fileHash.js create mode 100644 tests/osuConfig.js diff --git a/main.js b/main.js index d56be3a..b8d2278 100644 --- a/main.js +++ b/main.js @@ -7,16 +7,51 @@ const config = require("./src/config/config"); const { setupTitlebar, attachTitlebarToWindow } = require( "custom-electron-titlebar/main", ); -const { isValidOsuFolder } = require("./src/util/osuUtil"); +const { isValidOsuFolder, getUpdateFiles, getGlobalConfig, getFilesThatNeedUpdate, downloadUpdateFiles, getUserConfig, runOsuWithDevServer, getPatcherUpdates, downloadPatcherUpdates, getUIFiles, downloadUIFiles, replaceUIFile } = require("./src/util/osuUtil"); +const { formatBytes } = require("./src/util/formattingUtil"); +const windowName = require("get-window-by-name"); +const { existsSync } = require("fs"); +const { runFileDetached } = require("./src/util/executeUtil"); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let mainWindow; +let osuCheckInterval; +let userOsuPath; +let osuLoaded = false; + +let currentUser = undefined; function isDev() { return !app.isPackaged; } +function startOsuStatus() { + osuCheckInterval = setInterval(async () => { + const osuWindowTitle = windowName.getWindowText("osu!.exe"); + if (osuWindowTitle.length < 0) { + console.log("No osu! window found"); + return; + } + const firstInstance = osuWindowTitle[0]; + if (firstInstance) { + if (!osuLoaded) { + osuLoaded = true; + setTimeout(() => { + const patcherExecuteable = path.join(userOsuPath, "EZPPLauncher", "patcher.exe"); + if (existsSync(patcherExecuteable)) { + runFileDetached(userOsuPath, patcherExecuteable); + } + }, 3000); + } + } + }, 1000); +} + +function stopOsuStatus() { + clearInterval(osuCheckInterval); +} + function registerIPCPipes() { ipcMain.handle("ezpplauncher:login", async (e, args) => { const timeout = new AbortController(); @@ -43,6 +78,7 @@ function registerIPCPipes() { config.set("username", args.username); config.set("password", args.password); } + currentUser = args; config.remove("guest"); } return result; @@ -86,6 +122,12 @@ function registerIPCPipes() { if (fetchResult.ok) { const result = await fetchResult.json(); + if ("user" in result) { + currentUser = { + username: username, + password: password, + }; + } return result; } return { @@ -104,12 +146,14 @@ function registerIPCPipes() { config.remove("username"); config.remove("password"); config.set("guest", "1"); + currentUser = undefined; }); ipcMain.handle("ezpplauncher:logout", (e) => { config.remove("username"); config.remove("password"); config.remove("guest"); + currentUser = undefined return true; }); @@ -140,12 +184,22 @@ function registerIPCPipes() { return config.all(); }); - ipcMain.handle("ezpplauncher:launch", async (e) => { + ipcMain.handle("ezpplauncher:launch", async (e, args) => { + const patch = args.patch; mainWindow.webContents.send("ezpplauncher:launchstatus", { status: "Checking osu! directory...", }); await new Promise((res) => setTimeout(res, 1000)); const osuPath = config.get("osuPath"); + userOsuPath = osuPath; + if (osuPath == undefined) { + mainWindow.webContents.send("ezpplauncher:launchabort"); + mainWindow.webContents.send("ezpplauncher:alert", { + type: "error", + message: "osu! path not set!", + }); + return; + } if (!(await isValidOsuFolder(osuPath))) { mainWindow.webContents.send("ezpplauncher:launchabort"); mainWindow.webContents.send("ezpplauncher:alert", { @@ -158,6 +212,114 @@ function registerIPCPipes() { status: "Checking for osu! updates...", }); await new Promise((res) => setTimeout(res, 1000)); + const releaseStream = await getGlobalConfig(osuPath).get("_ReleaseStream"); + const latestFiles = await getUpdateFiles(releaseStream); + const uiFiles = await getUIFiles(osuPath); + const updateFiles = await getFilesThatNeedUpdate(osuPath, latestFiles); + if (uiFiles.length > 0) { + const uiDownloader = downloadUIFiles(osuPath, uiFiles); + uiDownloader.eventEmitter.on("data", (data) => { + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: Math.ceil(data.progress), + }); + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: `Downloading ${data.fileName}(${formatBytes(data.loaded)}/${formatBytes(data.total)})...`, + }); + }); + await uiDownloader.startDownload(); + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: -1, + }); + } + if (updateFiles.length > 0) { + const updateDownloader = downloadUpdateFiles(osuPath, updateFiles); + updateDownloader.eventEmitter.on("data", (data) => { + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: Math.ceil(data.progress), + }); + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: `Downloading ${data.fileName}(${formatBytes(data.loaded)}/${formatBytes(data.total)})...`, + }); + }); + await updateDownloader.startDownload(); + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: -1, + }); + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "osu! is now up to date!", + }); + await new Promise((res) => setTimeout(res, 1000)); + } else { + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "osu! is up to date!", + }); + await new Promise((res) => setTimeout(res, 1000)); + } + + if (patch) { + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Looking for patcher updates...", + }); + await new Promise((res) => setTimeout(res, 1000)); + const patchFiles = await getPatcherUpdates(osuPath); + if (patchFiles.length > 0) { + const patcherDownloader = downloadPatcherUpdates(osuPath, patchFiles); + patcherDownloader.eventEmitter.on("data", (data) => { + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: Math.ceil(data.progress), + }); + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: `Downloading ${data.fileName}(${formatBytes(data.loaded)}/${formatBytes(data.total)})...`, + }); + }); + await patcherDownloader.startDownload(); + mainWindow.webContents.send("ezpplauncher:launchprogress", { + progress: -1, + }) + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Patcher is now up to date!", + }); + } else { + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Patcher is up to date!", + }); + } + await new Promise((res) => setTimeout(res, 1000)); + } + + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Preparing launch...", + }); + + //TODO: save credentials to osu!.%username%.cfg + if (currentUser) { + const userConfig = getUserConfig(osuPath); + await userConfig.set("Username", currentUser.username); + await userConfig.set("Password", currentUser.password); + } + + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Launching osu!...", + }); + + const onExitHook = () => { + mainWindow.show(); + stopOsuStatus(); + mainWindow.webContents.send("ezpplauncher:launchstatus", { + status: "Waiting for cleanup...", + }); + + setTimeout(async () => { + await replaceUIFile(osuPath, true); + mainWindow.webContents.send("ezpplauncher:launchabort"); + }, 5000); + } + await replaceUIFile(osuPath, false); + runOsuWithDevServer(osuPath, "ez-pp.farm", onExitHook); + mainWindow.hide(); + startOsuStatus(); + + /* mainWindow.webContents.send("ezpplauncher:launchprogress", { progress: 0, }); diff --git a/package-lock.json b/package-lock.json index ced0649..f5453a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,8 +12,10 @@ "@types/better-sqlite3": "^7.6.8", "axios": "^1.6.5", "better-sqlite3": "^9.2.2", + "crypto": "^1.0.1", "custom-electron-titlebar": "^4.2.7", "electron-serve": "^1.1.0", + "get-window-by-name": "^2.0.0", "svelte-french-toast": "^1.2.0" }, "devDependencies": { @@ -3049,6 +3051,12 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in." + }, "node_modules/css-declaration-sorter": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.4.1.tgz", @@ -4291,6 +4299,78 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-window-by-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-window-by-name/-/get-window-by-name-2.0.0.tgz", + "integrity": "sha512-VXszlUFwkmWAZzxEERRJisiVvGMeB+Zjl5I9f0mwJjhfLTOkD5n5OR9Z518XBZemKLRzIs91TlfKbPmZywS84Q==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.2.1", + "cross-spawn": "^6.0.5", + "nan": "^2.0.5" + } + }, + "node_modules/get-window-by-name/node_modules/cross-spawn": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", + "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/get-window-by-name/node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/get-window-by-name/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/get-window-by-name/node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dependencies": { + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-window-by-name/node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-window-by-name/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, "node_modules/github-from-package": { "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", @@ -4905,8 +4985,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/jackspeak": { "version": "2.3.6", @@ -5553,6 +5632,11 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz", + "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w==" + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -5585,6 +5669,11 @@ "node": ">= 0.6" } }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" + }, "node_modules/node-abi": { "version": "3.54.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.54.0.tgz", diff --git a/package.json b/package.json index 79d50ec..8fdbf5b 100644 --- a/package.json +++ b/package.json @@ -37,8 +37,10 @@ "@types/better-sqlite3": "^7.6.8", "axios": "^1.6.5", "better-sqlite3": "^9.2.2", + "crypto": "^1.0.1", "custom-electron-titlebar": "^4.2.7", "electron-serve": "^1.1.0", + "get-window-by-name": "^2.0.0", "svelte-french-toast": "^1.2.0" }, "devDependencies": { diff --git a/preload.js b/preload.js index b6e29f9..7e15fd6 100644 --- a/preload.js +++ b/preload.js @@ -37,8 +37,8 @@ window.addEventListener("guest-login", async () => { await ipcRenderer.invoke("ezpplauncher:guestlogin"); }); -window.addEventListener("launch", async () => { - await ipcRenderer.invoke("ezpplauncher:launch"); +window.addEventListener("launch", async (e) => { + await ipcRenderer.invoke("ezpplauncher:launch", e.detail); }); window.addEventListener("settings-get", async () => { diff --git a/src/App.svelte b/src/App.svelte index bc47fbd..7c60e2b 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -52,7 +52,7 @@ launchPercentage.set(progress); }); - window.addEventListener("launchabort", () => { + window.addEventListener("launch-abort", () => { launchPercentage.set(-1); launchStatus.set(""); launching.set(false); diff --git a/src/pages/Launch.svelte b/src/pages/Launch.svelte index 14da0f4..78fd6ca 100644 --- a/src/pages/Launch.svelte +++ b/src/pages/Launch.svelte @@ -15,7 +15,7 @@ const launch = () => { launching.set(true); - window.dispatchEvent(new CustomEvent("launch")); + window.dispatchEvent(new CustomEvent("launch", { detail: { patch: $patch } }));; }; diff --git a/src/pages/Settings.svelte b/src/pages/Settings.svelte index 209130a..ac34dd3 100644 --- a/src/pages/Settings.svelte +++ b/src/pages/Settings.svelte @@ -42,11 +42,10 @@ >
- currentPage.set(Page.Launch)}>Go Back
diff --git a/src/util/executeUtil.js b/src/util/executeUtil.js new file mode 100644 index 0000000..e563e81 --- /dev/null +++ b/src/util/executeUtil.js @@ -0,0 +1,20 @@ +const childProcess = require("child_process"); + +const runFile = (folder, file, args, onExit) => { + childProcess.execFile(file, args, { + cwd: folder + }, (_err, _stdout, _stdin) => { + if (onExit) onExit(); + }) +} + +const runFileDetached = (folder, file, args) => { + const subProcess = childProcess.spawn(file + (args ? " " + args : ''), { + cwd: folder, + detached: true, + stdio: 'ignore' + }); + subProcess.unref(); +} + +module.exports = { runFile, runFileDetached }; \ No newline at end of file diff --git a/src/util/formattingUtil.js b/src/util/formattingUtil.js new file mode 100644 index 0000000..b60a9d0 --- /dev/null +++ b/src/util/formattingUtil.js @@ -0,0 +1,13 @@ +function formatBytes(bytes, decimals = 2) { + if (!+bytes) return '0 Bytes' + + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'] + + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))}${sizes[i]}` +} + +module.exports = { formatBytes }; \ No newline at end of file diff --git a/src/util/mathUtil.ts b/src/util/mathUtil.ts index 1dc5475..13e78ad 100644 --- a/src/util/mathUtil.ts +++ b/src/util/mathUtil.ts @@ -1,3 +1,3 @@ export const clamp = (val: number, min: number, max: number) => { return Math.max(min, Math.min(val, max)); -}; +}; \ No newline at end of file diff --git a/src/util/osuUtil.js b/src/util/osuUtil.js index f0211bc..fc41b97 100644 --- a/src/util/osuUtil.js +++ b/src/util/osuUtil.js @@ -1,5 +1,9 @@ const fs = require("fs"); const path = require("path"); +const crypto = require("crypto"); +const EventEmitter = require("events"); +const { default: axios } = require("axios"); +const { runFile } = require("./executeUtil"); const checkUpdateURL = "https://osu.ppy.sh/web/check-updates.php?action=check&stream="; @@ -30,6 +34,27 @@ const osuEntities = [ "scores.db", ]; +const patcherFiles = [ + { + name: "patcher.exe", + url_download: "https://ez-pp.farm/assets/patcher.exe", + url_hash: "https://ez-pp.farm/assets/patcher.md5" + }, + { + name: "patch.hook.dll", + url_download: "https://ez-pp.farm/assets/patch.hook.dll", + url_hash: "https://ez-pp.farm/assets/patch.hook.md5" + } +] + +const uiFiles = [ + { + name: "ezpp!ui.dll", + url_download: "https://ez-pp.farm/assets/ezpp!ui.dll", + url_hash: "https://ez-pp.farm/assets/ezpp!ui.md5" + } +] + async function isValidOsuFolder(path) { const allFiles = await fs.promises.readdir(path); let matches = 0; @@ -39,10 +64,71 @@ async function isValidOsuFolder(path) { return (Math.round((matches / osuEntities.length) * 100) >= 60); } -async function getUserConfig(osuPath) { +function getGlobalConfig(osuPath) { const configFileInfo = { name: "", path: "", + get: async (key) => { + if (!configFileInfo.path) { + console.log("config file not loaded"); + return ""; + } + const fileStream = await fs.promises.readFile( + configFileInfo.path, + "utf-8", + ); + const lines = fileStream.split(/\r?\n/); + for (const line of lines) { + if (line.includes(" = ")) { + const argsPair = line.split(" = ", 2); + const keyname = argsPair[0]; + const value = argsPair[1]; + if (keyname == key) { + return value; + } + } + } + }, + }; + const globalOsuConfig = path.join(osuPath, "osu!.cfg"); + if (fs.existsSync(globalOsuConfig)) { + configFileInfo.name = "osu!.cfg"; + configFileInfo.path = globalOsuConfig; + } + return configFileInfo; +} + +function getUserConfig(osuPath) { + const configFileInfo = { + name: "", + path: "", + set: async (key, newValue) => { + if (!configFileInfo.path) { + return ""; + } + const fileContents = []; + const fileStream = await fs.promises.readFile( + configFileInfo.path, + "utf-8", + ); + const lines = fileStream.split(/\r?\n/); + for (const line of lines) { + if (line.includes(" = ")) { + const argsPair = line.split(" = ", 2); + const keyname = argsPair[0]; + if (keyname == key) { + fileContents.push(`${key} = ${newValue}`); + } else { + fileContents.push(line); + } + } else { + fileContents.push(line); + } + } + await fs.promises.writeFile(configFileInfo.path, fileContents.join("\n")); + + return true; + }, get: async (key) => { if (!configFileInfo.path) { return ""; @@ -80,4 +166,194 @@ async function getUpdateFiles(releaseStream) { return releaseData.ok ? await releaseData.json() : undefined; } -module.exports = { isValidOsuFolder, getUserConfig, getUpdateFiles }; +async function getFilesThatNeedUpdate(osuPath, releaseStreamFiles) { + const updateFiles = []; + for (const updatePatch of releaseStreamFiles) { + const fileName = updatePatch.filename; + const fileHash = updatePatch.file_hash; + + const fileOnDisk = path.join(osuPath, fileName); + if (fs.existsSync(fileOnDisk)) { + if (ignoredOsuEntities.includes(fileName)) continue; + const fileHashOnDisk = crypto.createHash("md5").update(fs.readFileSync(fileOnDisk)).digest("hex"); + if (fileHashOnDisk.trim().toLowerCase() != fileHash.trim().toLowerCase()) { + console.log({ + fileOnDisk, + fileHashOnDisk, + fileHash + }) + updateFiles.push(updatePatch); + } + } else updateFiles.push(updatePatch); + } + + return updateFiles; +} + +function downloadUpdateFiles(osuPath, updateFiles) { + const eventEmitter = new EventEmitter(); + + const startDownload = async () => { + for (const updatePatch of updateFiles) { + const fileName = updatePatch.filename; + const fileSize = updatePatch.filesize; + const fileURL = updatePatch.url_full; + + const axiosDownloadWithProgress = await axios.get(fileURL, { + responseType: "stream", + onDownloadProgress: (progressEvent) => { + const { loaded, total } = progressEvent; + eventEmitter.emit("data", { + fileName, + loaded, + total, + progress: Math.floor((loaded / total) * 100) + }); + }, + }); + axiosDownloadWithProgress.data.on("end", () => { + eventEmitter.emit("data", { + fileName, + loaded: fileSize, + total: fileSize, + progress: 100 + }); + }) + await fs.promises.writeFile(path.join(osuPath, fileName), axiosDownloadWithProgress.data); + } + + // wait until all files are downloaded + return true; + } + + return { + eventEmitter, + startDownload, + } +} + +function runOsuWithDevServer(osuPath, serverDomain, onExit) { + const osuExecuteable = path.join(osuPath, "osu!.exe"); + runFile(osuPath, osuExecuteable, ["-devserver", serverDomain], onExit); +} + +async function getPatcherUpdates(osuPath) { + + const filesToDownload = []; + + const patcherDir = path.join(osuPath, "EZPPLauncher"); + if (!fs.existsSync(patcherDir)) fs.mkdirSync(patcherDir); + + for (const patcherFile of patcherFiles) { + if (fs.existsSync(path.join(patcherDir, patcherFile.name))) { + const latestPatchFileHash = await (await fetch(patcherFile.url_hash)).text(); + const localPatchFileHash = crypto.createHash("md5").update(fs.readFileSync(path.join(patcherDir, patcherFile.name))).digest("hex"); + if (latestPatchFileHash.trim().toLowerCase() != localPatchFileHash.trim().toLowerCase()) filesToDownload.push(patcherFile); + } else filesToDownload.push(patcherFile); + } + + return filesToDownload; +} + +function downloadPatcherUpdates(osuPath, patcherUpdates) { + const eventEmitter = new EventEmitter(); + + const startDownload = async () => { + + const patcherDir = path.join(osuPath, "EZPPLauncher"); + if (!fs.existsSync(patcherDir)) fs.mkdirSync(patcherDir); + + for (const patcherFile of patcherUpdates) { + const fileName = patcherFile.name; + const fileURL = patcherFile.url_download; + const axiosDownloadWithProgress = await axios.get(fileURL, { + responseType: "stream", + onDownloadProgress: (progressEvent) => { + const { loaded, total } = progressEvent; + eventEmitter.emit("data", { + fileName, + loaded, + total, + progress: Math.floor((loaded / total) * 100) + }); + }, + }); + + await fs.promises.writeFile(path.join(osuPath, "EZPPLauncher", fileName), axiosDownloadWithProgress.data); + } + } + + return { + eventEmitter, + startDownload, + } +} + +async function getUIFiles(osuPath) { + + const filesToDownload = []; + + const ezppLauncherDir = path.join(osuPath, "EZPPLauncher"); + if (!fs.existsSync(ezppLauncherDir)) fs.mkdirSync(ezppLauncherDir); + + for (const uiFile of uiFiles) { + if (fs.existsSync(path.join(ezppLauncherDir, uiFile.name))) { + const latestPatchFileHash = await (await fetch(uiFile.url_hash)).text(); + const localPatchFileHash = crypto.createHash("md5").update(fs.readFileSync(path.join(ezppLauncherDir, uiFile.name))).digest("hex"); + if (latestPatchFileHash.trim().toLowerCase() != localPatchFileHash.trim().toLowerCase()) filesToDownload.push(uiFile); + } else filesToDownload.push(uiFile); + } + + return filesToDownload; +} + +function downloadUIFiles(osuPath, uiFiles) { + const eventEmitter = new EventEmitter(); + + const startDownload = async () => { + + const ezpplauncherDir = path.join(osuPath, "EZPPLauncher"); + if (!fs.existsSync(ezpplauncherDir)) fs.mkdirSync(ezpplauncherDir); + + for (const uiFile of uiFiles) { + const fileName = uiFile.name; + const fileURL = uiFile.url_download; + const axiosDownloadWithProgress = await axios.get(fileURL, { + responseType: "stream", + onDownloadProgress: (progressEvent) => { + const { loaded, total } = progressEvent; + eventEmitter.emit("data", { + fileName, + loaded, + total, + progress: Math.floor((loaded / total) * 100) + }); + }, + }); + + await fs.promises.writeFile(path.join(osuPath, "EZPPLauncher", fileName), axiosDownloadWithProgress.data); + } + } + + return { + eventEmitter, + startDownload, + } +} + +async function replaceUIFile(osuPath, revert) { + if (!revert) { + const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll"); + const oldOsuUIFile = path.join(osuPath, "osu!ui.dll"); + await fs.promises.rename(oldOsuUIFile, path.join(osuPath, "osu!ui.dll.bak")); + await fs.promises.rename(ezppUIFile, oldOsuUIFile); + } else { + const oldOsuUIFile = path.join(osuPath, "osu!ui.dll"); + const ezppUIFile = path.join(osuPath, "EZPPLauncher", "ezpp!ui.dll"); + await fs.promises.rename(oldOsuUIFile, ezppUIFile); + await fs.promises.rename(path.join(osuPath, "osu!ui.dll.bak"), oldOsuUIFile); + } + +} + +module.exports = { isValidOsuFolder, getUserConfig, getGlobalConfig, getUpdateFiles, getFilesThatNeedUpdate, downloadUpdateFiles, runOsuWithDevServer, getPatcherUpdates, downloadPatcherUpdates, downloadUIFiles, getUIFiles, replaceUIFile }; diff --git a/tests/fileHash.js b/tests/fileHash.js new file mode 100644 index 0000000..f2ba454 --- /dev/null +++ b/tests/fileHash.js @@ -0,0 +1,14 @@ +const fs = require("fs"); +const crypto = require("crypto"); + +(async () => { + const correctHash = 'b66478cc0f9ec50810489a039ced642b'; + const filePath = 'C:\\Users\\horiz\\AppData\\Local\\osu!\\avcodec-51.dll'; + const fileHash = crypto.createHash('md5').update(await fs.promises.readFile(filePath)).digest('hex'); + + console.log({ + correctHash, + fileHash, + matching: correctHash === fileHash, + }) +})(); \ No newline at end of file diff --git a/tests/osuConfig.js b/tests/osuConfig.js new file mode 100644 index 0000000..7365164 --- /dev/null +++ b/tests/osuConfig.js @@ -0,0 +1,9 @@ +const { getGlobalConfig } = require("../src/util/osuUtil"); +const config = require("../src/config/config"); +(async () => { + const osuPath = config.get("osuPath"); + const globalConfig = getGlobalConfig(osuPath); + + const globalConfigContent = await globalConfig.get("_ReleaseStream"); + console.log(globalConfigContent); +})(); \ No newline at end of file