const { BrowserWindow, app, net, dialog, ipcMain, nativeImage, session, shell } = require("electron") const msmc = require("msmc") const os = require("node:os") const fs = require("node:fs") const path = require("node:path") const hwid = require("./modules/hwid") const serverPing = require("./modules/serverPing") const gameOptions = require("./modules/gameOptions") const launcherSettings = require("./modules/launcherSettings") const config = require("./config.json") const { Authenticator, Client } = require("minecraft-launcher-core") const fileManager = require("./modules/fileManager") const launcher = new Client() const { io } = require("socket.io-client") const download = require("download") const socket = io({ host: config.api.websockets.base.host, port: config.api.websockets.base.port }) const { ChildProcess } = require("child_process") let launcherWindow, auth, gamePlayable = false, launchProcess async function createLauncherWindow() { gameOptions.initOptions(path.join(app.getPath("appData"), ".catboat", "options.txt")) launcherSettings.initSettings(path.join(app.getPath("appData"), ".catboat")) if (net.isOnline()) { let win_width = 1550 let win_height = parseInt(win_width / (16/9)) const isLauncherNotBanned = await checkIfIAmBanned() try { launcherWindow = new BrowserWindow({ frame: false, width: win_width, height: win_height, minWidth: win_width, minHeight: win_height, titleBarStyle: "hidden", autoHideMenuBar: true, roundedCorners: false, resizable: false, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, "modules", "preload.js"), webviewTag: true } }) if (os.platform() == "darwin") { app.dock.setIcon(nativeImage.createFromPath(path.join(__dirname, "app", "assets", "img", "icon.png"))) } launcherWindow.setIcon(path.join(__dirname, "app", "assets", "img", "icon.png")) if (isLauncherNotBanned.success) { launcherWindow.loadFile(path.join(__dirname, "app", "login.html")) session.defaultSession.webRequest.onBeforeRequest({ urls: [ "https://embed.twitch.tv/*channel=*" ] }, (details, callback) => { const url = details.url const urlParams = new URLSearchParams(url.replace("https://embed.twitch.tv/", "")) if (urlParams.get("parent")) { callback({}) return } urlParams.set("parent", "localhost") urlParams.set("referrer", "https://localhost/") const redirectUrl = `https://embed.twitch.tv/?${urlParams.toString()}` callback({ cancel: false, redirectURL: redirectUrl }) }) session.defaultSession.webRequest.onHeadersReceived({ urls: [ "https://www.twitch.tv/*", "https://player.twitch.tv/*", "https://embed.twitch.tv/*" ] }, (details, callback) => { const responseHeaders = details.responseHeaders delete responseHeaders["Content-Security-Policy"] callback({ cancel: false, responseHeaders }) }) launcherWindow.webContents.openDevTools() } else { launcherWindow.loadFile(path.join(__dirname, "app", "banned.html")) launcherWindow.webContents.executeJavaScript(` setBannedBy("${isLauncherNotBanned.banned_by}") setBannedAt("${isLauncherNotBanned.banned_at}") setBannedBecause("${isLauncherNotBanned.reason}") `) } } catch (error) { dialog.showErrorBox("Erreur", error.toString()) } } else { dialog.showErrorBox("Connexion internet", "Le launcher requiert une connexion internet.") } } async function checkIfIAmBanned() { if (net.isOnline()) { try { const reponse = await fetch(`${config.api.base}${config.api.endpoints.checkBanStatus}`, { method: "post", headers: { accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify({ hwid: hwid.getHWID() }) }) const json = await reponse.json() return json } catch (error) { dialog.showErrorBox("Connexion à l'API", "Impossible de contacter l'API, fermeture du launcher.") console.error(error) app.exit() } } else { dialog.showErrorBox("Connexion internet", "Le launcher requiert une connexion internet.") } } app.whenReady().then(() => { createLauncherWindow() app.on("activate", async () => { if (BrowserWindow.getAllWindows().length === 0) await createLauncherWindow() }) }) app.on("window-all-closed", () => { app.quit() }) ipcMain.on("call", async (event, data) => { switch (data.method) { case "hardware::ramInformation": launcherWindow.webContents.send("Response", { totalRam: Math.round(os.totalmem() / 1024 / 1024 * 100) / 100, avaibleRam: Math.round(os.freemem() / 1024 / 1024 * 100) / 100, }) break case "server::ping": const status = await serverPing.fetchServerStatus() launcherWindow.webContents.send("Response", status) break case "window::close": launcherWindow.close() app.quit() break case "skin::set": const file = await dialog.showOpenDialog(launcherWindow, { filters: [ { name: "Images", extensions: ["png", "jpg", "jpeg"] } ], properties: ["openFile", "dontAddToRecent", "showHiddenFiles"], title: "Sélectionner l'image de votre skin", securityScopedBookmarks: true }) if (!file.canceled) { const confirmDialog = await dialog.showMessageBox(launcherWindow, { type: "question", message: "Êtes-vous sûr de vouloir changer votre skin ?", buttons: [ "Oui", "Annuler" ], title: "Confirmer le changement de skin" }) confirmDialog.response == 0 ? skinPath = file.filePaths[0] : skinPath = null } break case "auth::mojang": if (data.args.trim() != "") { auth = Authenticator.getAuth(data.args.username, data.args.password) await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${(await auth).uuid}`) await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) } else { dialog.showErrorBox("Erreur", "Le mot de passe n'est pas défini. Les comptes crackés ne sont pas supporté par le launcher.") } break case "auth::microsoft": const authManager = new msmc.Auth("select_account") try { const xboxManager = await authManager.launch("raw") const token = await xboxManager.getMinecraft() auth = token.mclc() await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${auth.uuid}`) await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) } catch (error) { console.error(error) if (error == "error.gui.closed") { launcherWindow.webContents.send("Response") } } break case "auth::refresh": const user = data.args.user if (user.meta?.type == "msa") { try { const authManager = new msmc.Auth("none") const xboxManager = await authManager.refresh(user.meta.refresh) const minecraft = await xboxManager.getMinecraft() auth = minecraft.mclc() await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${auth.uuid}`) } catch (error) { dialog.showErrorBox("Erreur lors de la connexion via token", error) console.error(error) launcherWindow.webContents.send("Response") } } else { try { auth = Authenticator.refreshAuth(user.access_token, user.client_token) await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${(await auth).uuid}`) } catch (error) { dialog.showErrorBox("Erreur lors de la connexion via token", error) console.error(error) launcherWindow.webContents.send("Response") } } break case "shell::openExternal": shell.openExternal(data.args.url) break case "audio::mute": launcherWindow.webContents.setAudioMuted(true) break case "audio::unmute": launcherWindow.webContents.setAudioMuted(false) break case "player::profile": await launcherWindow.webContents.send("Response", auth) break case "game::parseOptions": const $gameOptions = gameOptions.parseOptions() const allowedKeys = ["renderDistance", "renderClouds", "graphicsMode", "gamma", "graphicsMode", "guiScale"] const filteredOptions = Object.fromEntries( Object.entries($gameOptions).filter(([key]) => allowedKeys.includes(key)) ) launcherWindow.webContents.send("Response", filteredOptions) break case "game::optionSet": const options = gameOptions.parseOptions() switch (data.args.key) { case "graphicsMode": options.graphicsMode = data.args.key == true ? 0 : 1 break case "gamma": options.gamma = data.args.key == false ? 0.50 : 1.0 break default: options[data.args.key] = data.args.value break } gameOptions.saveOptions(gameOptions.stringfyOptions(options)) break case "settings::read": const $launcherSettings = launcherSettings.readSettings() launcherWindow.webContents.send("Response", $launcherSettings) break case "game::launch": launchGame() break case "app::devtools": launcherWindow.webContents.openDevTools() break } }) async function launchGame(restartGame) { launcherWindow.webContents.send("Response", { disablePlayButton: false }) const downloadQueue = [] const remoteFiles = await fileManager.getRemoteFiles() const localFiles = fs.readdirSync(path.join(app.getPath("appData"), ".catboat"), { recursive: true }) launcherWindow.setProgressBar(10, { mode: "indeterminate" }) for (const remoteFile of remoteFiles) { try { const localFile = localFiles.find(file => file === remoteFile) if (!localFile) { downloadQueue.push(remoteFile) continue } const localHash = await fileManager.getFileHash(path.join(app.getPath("appData"), ".catboat", localFile)) const remoteHash = await fileManager.getRemoteFileHash(localFile) if (localHash != remoteHash) { downloadQueue.push(localFile) } } catch (error) { console.error(error) break } } for (const localFile of localFiles) { if (!remoteFiles.find(remoteFile => remoteFile.path == localFile) && localFile.startsWith("/mods")) { fs.unlinkSync(path.join(app.getPath("appData"), ".catboat", localFile)) } } for (const item of downloadQueue) { const url = `${config.api.base}${config.api.endpoints.downloadFile}/${new String(item).replace(/\\/g, "/")}` try { await download(url, path.join(app.getPath("appData"), ".catboat", path.dirname(item))) launcherWindow.setProgressBar(((downloadQueue.indexOf(item) + 1) / downloadQueue.length) * 100, { mode: "normal" }) } catch (error) { launcherWindow.setProgressBar(((downloadQueue.indexOf(item) + 1) / downloadQueue.length) * 100, { mode: "error" }) dialog.showErrorBox("Erreur lors du téléchargement des fichiers", error.toString()) continue } } launcherWindow.setProgressBar(0, { mode: "none" }) dialog.showMessageBox(launcherWindow, { title: "Téléchargement des fichiers", message: "Téléchargement fini." }) gamePlayable = true launcher.on("close", () => { launcherSettings.webContents.send("Response", { disablePlayButton: true }) gamePlayable = true }) launcher.on("debug", (log) => { console.log(log) }) launcher.on("data", (log) => { console.log(log) }) if (gamePlayable || restartGame) { const $launchProcess = await launcher.launch({ root: path.join(app.getPath("appData"), ".catboat"), authorization: auth, version: { number: "1.16.5", type: "release" }, forge: path.join(app.getPath("appData"), ".catboat", "forge-1.16.5.jar"), memory: { min: 512, max: launcherSettings.get("ram").max } }) launchProcess = $launchProcess } } socket.on("force-game-update", async () => { if (typeof launchProcess != "undefined") { dialog.showMessageBox(launcherWindow, { title: "Mise à jours forcé", message: "Une fois que vous aurez cliquer sur ok, le jeu se fermera et mettra à jours ses fichiers pour se relancer en suite." }) launchProcess.kill() await launchGame(true) } })