2025-05-11 20:33:07 +02:00

487 lines
21 KiB
JavaScript

const { BrowserWindow, app, net, dialog, ipcMain, nativeImage, session, shell } = require("electron")
const properties = require("js-java-properties")
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 rpc = require("./modules/rpc")
const java = require("./modules/java")
const socket = io({
host: config.api.websockets.base.host,
port: config.api.websockets.base.port
})
let launcherWindow, defaultWindow, auth, gamePlayable = false, launchProcess
rpc.startRichPresence()
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,
devTools: false
}
})
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 createDefaultWindow() {
gameOptions.initOptions(path.join(app.getPath("appData"), ".catboat", "options.txt"))
launcherSettings.initSettings(path.join(app.getPath("appData"), ".catboat"))
if (net.isOnline()) {
const isLauncherNotBanned = await checkIfIAmBanned()
try {
defaultWindow = new BrowserWindow({
frame: false,
width: 300,
height: 400,
minWidth: 300,
minHeight: 400,
titleBarStyle: "hidden",
autoHideMenuBar: true,
roundedCorners: false,
resizable: false,
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: path.join(__dirname, "modules", "preload.js"),
webviewTag: true,
devTools: false
}
})
if (os.platform() == "darwin") {
app.dock.setIcon(nativeImage.createFromPath(path.join(__dirname, "app", "assets", "img", "icon.png")))
}
defaultWindow.setIcon(path.join(__dirname, "app", "assets", "img", "icon.png"))
if (isLauncherNotBanned.success) {
defaultWindow.loadFile(path.join(__dirname, "app", "loading.html"))
defaultWindow.webContents.openDevTools()
try {
await java.main(path.join(app.getPath("appData"), ".catboat", "jre"))
await launchGame(false)
defaultWindow.hide()
createLauncherWindow()
} catch (error) {
console.log(error)
defaultWindow.hide()
createLauncherWindow()
}
} else {
launcherWindow.loadFile(path.join(__dirname, "app", "banned.html"))
defaultWindow.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.")
}
}
async function checkCoutdown(uuid) {
try {
const response = await fetch(`${config.api.base}${config.api.endpoints.countdown}/${uuid}`)
const json = await response.json()
console.log(json)
return json.success
} catch (error) {
return false
}
}
function writeOculusConfigFile(filePath) {
const oculusProperties = properties.parse(fs.readFileSync(filePath).toString())
if (data.args.boolean) {
properties.setProperty(oculusProperties, "shaderPack", "Sildurs_Vibrant_Shaders_v1.50_Medium.zip")
} else {
properties.setProperty(oculusProperties, "shaderPack", "")
}
fs.writeFileSync(filePath, properties.stringify(oculusProperties))
}
app.whenReady().then(() => {
createDefaultWindow()
app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) await createDefaultWindow()
})
})
app.on("window-all-closed", () => {
app.quit()
})
ipcMain.on("call", async (event, data) => {
switch (data.method) {
case "hardware::ramInformation":
launcherWindow.webContents.send("Response<hardware::ramInformation>", {
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<server::ping>", status)
break
case "window::close":
launcherWindow.close()
app.quit()
break
case "window::reduce":
launcherWindow.minimize()
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)
launcherSettings.set("auth", { token: (await auth).access_token, type: "mojang", clientToken: (await auth).client_token })
if (await checkCoutdown((await auth).uuid)) {
await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html"))
await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${(await auth).uuid}`)
} else {
await launcherWindow.loadURL("https://nyancraft.catboat.fr")
await launcherWindow.webContents.insertCSS("a.download-button { display: none; } #return-button { app-region: no-drag } ")
await launcherWindow.webContents.executeJavaScript(`
const returnButton = document.querySelector(\"#return-button\")
returnButton.innerText = "Fermer le launcher"
returnButton.onclick = () => system.call("window::close")
`)
}
} 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("electron")
const token = await xboxManager.getMinecraft()
auth = token.mclc()
console.log(auth.meta)
launcherSettings.set("auth", { token: auth.meta, type: "msa", clientToken: auth.client_token })
if (await checkCoutdown(auth.uuid)) {
launcherWindow.loadFile(path.join(__dirname, "app", "logged.html"))
fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${auth.uuid}`)
} else {
await launcherWindow.loadURL("https://nyancraft.catboat.fr")
await launcherWindow.webContents.insertCSS("a.download-button { display: none; } #return-button { app-region: no-drag } ")
await launcherWindow.webContents.executeJavaScript(`
const returnButton = document.querySelector(\"#return-button\")
returnButton.innerText = "Fermer le launcher"
returnButton.onclick = () => system.call("window::close")
`)
}
} catch (error) {
console.error(error)
if (error == "error.gui.closed") {
launcherWindow.webContents.send("Response<auth::microsoft>")
}
}
break
case "auth::reset":
launcherSettings.set("auth", { token: "", type: "msa", clientToken: "" })
break
case "settings::set":
launcherSettings.set(data.args.key, data.args.value)
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<player::profile>", 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<game::parseOptions>", 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<settings::read>", $launcherSettings)
break
case "game::launch":
launchGame()
break
case "app::devtools":
launcherWindow.webContents.openDevTools()
break
case "oculus::defaultshaderset":
const filePath = path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")
if (fs.existsSync(filePath)) {
writeOculusConfigFile(filePath)
} else {
fs.copyFileSync(path.join(__dirname, "oculus.properties"), filePath)
writeOculusConfigFile(filePath)
}
break
case "oculus::getdefaultshaderset":
const $filePath = path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")
if (fs.existsSync($filePath)) {
const $oculusProperties = properties.parse(fs.readFileSync($filePath).toString())
launcherWindow.webContents.send("Response<oculus::getdefaultshaderset>", properties.getProperty($oculusProperties, "shaderPack") == "Sildurs_Vibrant_Shaders_v1.50_Medium.zip" ? true : false) } else {
const $oculusProperties = properties.parse(fs.readFileSync($filePath).toString())
launcherWindow.webContents.send("Response<oculus::getdefaultshaderset>", properties.getProperty($oculusProperties, "shaderPack") == "Sildurs_Vibrant_Shaders_v1.50_Medium.zip" ? true : false)
}
break
}
})
async function launchGame(restartGame) {
let sendToRenderer = false
if (launcherWindow instanceof BrowserWindow) {
sendToRenderer = true
launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: false })
}
const downloadQueue = []
const remoteFiles = await fileManager.getRemoteFiles()
const localFiles = fs.readdirSync(path.join(app.getPath("appData"), ".catboat"), { recursive: true })
if (launcherWindow instanceof BrowserWindow) {
launcherWindow.webContents.send("Response<progress::update>", { type: "landing" })
}
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)))
if (launcherWindow instanceof BrowserWindow) {
launcherWindow.webContents.send("Response<izitoast::info>", { title: "Lancement du jeu", message: "Ce processus peut être plus ou moins long selon votre configuration." })
launcherWindow.webContents.send("Response<progress::update>", { type: "continuous" })
}
} catch (error) {
if (launcherWindow instanceof BrowserWindow) {
launcherWindow.webContents.send("Response<progress::hide>")
launcherWindow.webContents.send("Response<izitoast::error>", { title: "Erreur", message: new String(error) })
}
dialog.showErrorBox("Erreur lors du téléchargement des fichiers", error.toString())
continue
}
}
gamePlayable = true
launcher.on("close", () => {
launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: true })
gamePlayable = true
})
launcher.on("debug", (log) => {
console.log(log)
})
launcher.on("data", (log) => {
if (sendToRenderer) {
launcherWindow.webContents.send("Response<game::launched>")
sendToRenderer = false
}
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"),
javaPath: await java.getPath(path.join(app.getPath("appData"), ".catboat", "jre")),
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)
} else {
await launchGame(false)
}
})