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.js"); const checkUpdateURL = "https://osu.ppy.sh/web/check-updates.php?action=check&stream="; const ignoredOsuEntities = [ "osu!auth.dll", ]; const osuEntities = [ "avcodec-51.dll", "avformat-52.dll", "avutil-49.dll", "bass.dll", "bass_fx.dll", "collection.db", "d3dcompiler_47.dll", "libEGL.dll", "libGLESv2.dll", "Microsoft.Ink.dll", "OpenTK.dll", "osu!.cfg", "osu!.db", "osu!.exe", "osu!auth.dll", "osu!gameplay.dll", "osu!seasonal.dll", "osu!ui.dll", "presence.db", "pthreadGC2.dll", "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; for (const file of allFiles) { if (osuEntities.includes(file)) matches = matches + 1; } return (Math.round((matches / osuEntities.length) * 100) >= 60); } function getGlobalConfig(osuPath) { const configFileInfo = { name: "", path: "", get: async (key) => { if (!configFileInfo.path) { 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 ""; } 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 userOsuConfig = path.join( osuPath, `osu!.${process.env["USERNAME"]}.cfg`, ); if (fs.existsSync(userOsuConfig)) { configFileInfo.name = `osu!.${process.env["USERNAME"]}.cfg`; configFileInfo.path = userOsuConfig; } return configFileInfo; } async function getUpdateFiles(releaseStream) { const releaseData = await fetch(checkUpdateURL + releaseStream); return releaseData.ok ? await releaseData.json() : undefined; } 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() ) { 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, }); }); try { if (fs.existsSync(path.join(osuPath, fileName))) { await fs.promises.rm(path.join(osuPath, fileName), { force: true, }); } await fs.promises.writeFile( path.join(osuPath, fileName), axiosDownloadWithProgress.data, ); } catch (err) { console.log(err); eventEmitter.emit("error", { fileName, }); } } // 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), }); }, }); try { if (fs.existsSync(path.join(osuPath, "EZPPLauncher", fileName))) { await fs.promises.rm(path.join(osuPath, "EZPPLauncher", fileName), { force: true, }); } await fs.promises.writeFile( path.join(osuPath, "EZPPLauncher", fileName), axiosDownloadWithProgress.data, ); } catch (err) { console.log(err); eventEmitter.emit("error", { fileName, }); } } }; 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), }); }, }); try { if (fs.existsSync(path.join(osuPath, "EZPPLauncher", fileName))) { await fs.promises.rm(path.join(osuPath, "EZPPLauncher", fileName), { force: true, }); } await fs.promises.writeFile( path.join(osuPath, "EZPPLauncher", fileName), axiosDownloadWithProgress.data, ); } catch (err) { console.log(err); eventEmitter.emit("error", { fileName, }); } } }; 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, ); } } async function findOsuInstallation() { const regedit = require("regedit-rs"); const osuLocationFromDefaultIcon = "HKLM\\SOFTWARE\\Classes\\osu\\DefaultIcon"; const osuKey = regedit.listSync(osuLocationFromDefaultIcon); if (osuKey[osuLocationFromDefaultIcon].exists) { const key = osuKey[osuLocationFromDefaultIcon].values[""]; let value = key.value; value = value.substring(1, value.length - 3); return path.dirname(value.trim()); } return undefined; } async function updateOsuConfigHashes(osuPath) { const osuCfg = path.join(osuPath, "osu!.cfg"); const fileStream = await fs.promises.readFile(osuCfg, "utf-8"); const lines = fileStream.split(/\r?\n/); const newLines = []; for (const line of lines) { if (line.includes(" = ")) { const argsPair = line.split(" = ", 2); const key = argsPair[0]; const value = argsPair[1]; if (key.startsWith("h_")) { const fileName = key.substring(2, key.length); const filePath = path.join(osuPath, fileName); if (!fs.existsSync(filePath)) continue; const binaryFileContents = await fs.promises.readFile(filePath); const existingFileMD5 = crypto.createHash("md5").update( binaryFileContents, ).digest("hex"); if (value == existingFileMD5) newLines.push(line); else newLines.push(`${key} = ${existingFileMD5}`); } else if (line.startsWith("u_UpdaterAutoStart")) { newLines.push(`${key} = 0`); } else { newLines.push(line); } } else { newLines.push(line); } } await fs.promises.writeFile(osuCfg, newLines.join("\n"), "utf-8"); } module.exports = { isValidOsuFolder, getUserConfig, getGlobalConfig, getUpdateFiles, getFilesThatNeedUpdate, downloadUpdateFiles, runOsuWithDevServer, getPatcherUpdates, downloadPatcherUpdates, downloadUIFiles, getUIFiles, replaceUIFile, findOsuInstallation, updateOsuConfigHashes, };