Compare commits

...

9 Commits

Author SHA1 Message Date
72354c053f Update package.json 2025-05-11 21:03:40 +02:00
5f8ba166cb Create build.yml 2025-05-11 20:43:47 +02:00
eb8f11ac89 sync 2025-05-11 20:33:07 +02:00
86bb37944c sync 2025-05-11 17:31:53 +02:00
cd627b03ae sync 2025-05-11 08:01:49 +02:00
1106ced049 sync 2025-05-11 07:58:45 +02:00
0c01e826d9 sync 2025-05-11 07:30:13 +02:00
48c5fd4ead Update main.js 2025-05-10 18:52:18 +02:00
d6fa5b69ce sync 2025-05-10 18:37:31 +02:00
15 changed files with 509 additions and 219 deletions

32
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Release app
on:
workflow_dispatch: null
jobs:
release:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os:
- windows-latest
- macos-latest
- macos-latest
- ubuntu-latest
steps:
- name: Check out Git repository
uses: actions/checkout@4.2.2
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@4.4.0
with:
node-version: 20
- name: Config git user
run: |
git config --global user.email "gilleslazure04@gmail.com"
git config --global user.name "Gilles Lazure <azures04>"
- name: Install app dependencies
run: |
npm i
- name: Build app
run: |
node build
env:
GITHUB_TOKEN: ${{ github.token }}

View File

@ -11,6 +11,14 @@ body {
background-color: #262626; background-color: #262626;
} }
main {
app-region: drag;
}
main > * {
app-region: no-drag;
}
main > article > section > img { main > article > section > img {
width: 150px; width: 150px;
} }
@ -33,6 +41,14 @@ main > article > section.informations {
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
} }
main > article > section.logo > h2 {
color: #ffffff;
padding: 13px 13px 13px 13px;
font-weight: bolder;
text-align: center;
font-family: "Roboto", sans-serif;
}
main > article > section.informations > h2 { main > article > section.informations > h2 {
margin-bottom: 10px; margin-bottom: 10px;
} }

View File

@ -4,6 +4,8 @@
* { * {
margin: 0px; margin: 0px;
padding: 0px; padding: 0px;
outline: none;
user-select: none;
box-sizing: border-box; box-sizing: border-box;
font-family: "Roboto", sans-serif; font-family: "Roboto", sans-serif;
} }
@ -16,6 +18,11 @@ body {
background-attachment: fixed; background-attachment: fixed;
} }
img {
app-region: drag;
pointer-events: none;
}
main { main {
position: absolute; position: absolute;
top: 0px; top: 0px;
@ -24,12 +31,19 @@ main {
bottom: 0px; bottom: 0px;
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: hidden;
app-region: drag;
}
main > * {
app-region: no-drag;
} }
main > nav { main > nav {
margin-left: 1rem; margin-left: 1rem;
width: 20%; width: 20%;
height: 100%; height: 100%;
overflow-y: auto;
background-color: #3e3e3ee6; background-color: #3e3e3ee6;
} }
@ -372,25 +386,63 @@ div.checkboxes > input[type="checkbox"] {
font-size: small; font-size: small;
} }
button.load { div.loader {
content: ""; overflow: hidden;
transition: background-color 0.3s; position: absolute;
left: 0px;
right: 0px;
bottom: 0px;
width: 100%;
height: 10px;
} }
div.loader > div.full {
height: 10px;
width: 100%;
background-color: #3e3e3ee6;
}
div.loader > div.full > div.progress,
div.loader > div.full > div.loading {
height: 10px;
width: 50%;
transition: 1s width;
background-color: #39aa6d;
}
div.loader > div.full > div.loading {
animation: animateLoadingEffect 1s linear infinite;
}
[hidden] { [hidden] {
display: none; display: none;
visibility: hidden; visibility: hidden;
} }
@keyframes backgroundAnimation {
/* Chrome-specific scrollbar styling */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.1); /* Slightly visible track for better contrast */
}
::-webkit-scrollbar-thumb {
background: #2E8B57;
border-radius: 4px; /* Rounded edges for a modern look */
}
::-webkit-scrollbar-thumb:hover {
background: #39aa6d;
}
@keyframes animateLoadingEffect {
0% { 0% {
background-position: 0% 50%; transform: translateX(-100%);
}
50% {
background-position: 100% 50%;
} }
100% { 100% {
background-position: 0% 50%; transform: translateX(200%);
} }
} }

View File

@ -58,18 +58,32 @@ function toggleAudio(element) {
function toggleMusic(element) { function toggleMusic(element) {
if (element.getAttribute("state") == 0) { if (element.getAttribute("state") == 0) {
system.call("audio::mute") audio.pause()
element.setAttribute("state", 1) element.setAttribute("state", 1)
element.children[0].classList.replace("fa-pause", "fa-play") element.children[0].classList.replace("fa-pause", "fa-play")
element.children[1].innerText = "Reprendre" element.children[1].innerText = "Reprendre"
} else { } else {
system.call("audio::unmute") audio.play()
element.setAttribute("state", 0) element.setAttribute("state", 0)
element.children[0].classList.replace("fa-play", "fa-pause") element.children[0].classList.replace("fa-play", "fa-pause")
element.children[1].innerText = "Pause" element.children[1].innerText = "Pause"
} }
} }
function toggleMusicVolume(element) {
if (element.getAttribute("state") == 0) {
system.call("audio::mute")
element.setAttribute("state", 1)
element.children[0].classList.replace("fa-volume", "fa-volume-slash")
element.children[1].innerText = "Activer le son"
} else {
system.call("audio::unmute")
element.setAttribute("state", 0)
element.children[0].classList.replace("fa-volume-slash", "fa-volume")
element.children[1].innerText = "Couper le son"
}
}
function updateVolume(value) { function updateVolume(value) {
audio.volume = value / 100 audio.volume = value / 100
audioPourcentageLabel.innerText = `${value}%` audioPourcentageLabel.innerText = `${value}%`
@ -86,7 +100,7 @@ system.result("server::ping", pong => {
function handleOptionsChanges(key, value) { function handleOptionsChanges(key, value) {
system.call("game::optionSet", { key, value }) system.call("game::optionSet", { key, value })
const span = document.querySelector(`span#${key}`) const span = document.querySelector(`span#current${key.replace(/./, c => c.toUpperCase())}`)
if (span) { if (span) {
span.innerText = Math.floor(value) span.innerText = Math.floor(value)
} }
@ -100,16 +114,66 @@ function handleSettingsChanges(key, value) {
} }
} }
function setLoadingType(type) {
const loader = document.querySelector(".loader")
switch (type) {
case "continuous":
loader.children[0].children[0].classList.remove("progress")
loader.children[0].children[0].classList.add("loading")
break
case "progress":
loader.children[0].children[0].classList.remove("loading")
loader.children[0].children[0].classList.add("progress")
break
default:
break
}
}
function setLoadingProgress(pourcentage) {
const loader = document.querySelector(".loader")
const progress = loader.querySelector(".progress")
if (progress) {
progress.setAttribute("style", `width: ${pourcentage}%`)
}
}
function toggleLoadingBar() {
const loader = document.querySelector(".loader")
if (loader.hasAttribute("hidden")) {
loader.removeAttribute("hidden")
} else {
loader.setAttribute("hidden", "")
}
}
function showLoadingBar() {
const loader = document.querySelector(".loader")
if (loader.hasAttribute("hidden")) {
loader.removeAttribute("hidden")
}
}
function hideLoadingBar() {
const loader = document.querySelector(".loader")
if (!loader.hasAttribute("hidden")) {
loader.setAttribute("hidden", "")
}
}
system.result("game::parseOptions", options => { system.result("game::parseOptions", options => {
gamma.checked = options.gamma == 1 ? true : false gamma.checked = options.gamma == 1 ? true : false
renderClouds.checked = options.renderrenderClouds == false ? false : true renderClouds.checked = options.renderrenderClouds == false ? false : true
guiScale.value = options.guiScale guiScale.value = options.guiScale
currentGuiScale.innerText = options.guiScale
graphicsMode.checked = options.graphicsMode == 0 ? true : false graphicsMode.checked = options.graphicsMode == 0 ? true : false
renderDistance.value = options.renderDistance renderDistance.value = options.renderDistance
currentRenderDistance.innerText = options.renderDistance
}) })
system.result("settings::read", settings => { system.result("settings::read", settings => {
ram.value = settings.ram.max ram.value = settings.ram.max
currentRam.innerText = `${Math.floor(settings.ram.max / 1024)} G`
}) })
system.result("hardware::ramInformation", $ram => { system.result("hardware::ramInformation", $ram => {
@ -120,11 +184,17 @@ system.result("hardware::ramInformation", $ram => {
system.result("game::launch", info => { system.result("game::launch", info => {
if (info.disablePlayButton) { if (info.disablePlayButton) {
playButton.removeAttribute("hidden") playButton.removeAttribute("hidden")
hideLoadingBar()
} else { } else {
playButton.setAttribute("hidden", "") playButton.setAttribute("hidden", "")
} }
}) })
system.result("game::launched", info => {
muteAudio()
hideLoadingBar()
})
system.result("player::profile", playerProfile => { system.result("player::profile", playerProfile => {
console.log(playerProfile) console.log(playerProfile)
}) })
@ -133,6 +203,26 @@ system.result("oculus::getdefaultshaderset", boolean => {
sildurs_shader.checked = boolean sildurs_shader.checked = boolean
}) })
system.result("progress::update", info => {
showLoadingBar()
setLoadingType(info.type)
if (info.type == "progress" && typeof info.pourcentage == "number") {
setLoadingProgress(info.pourcentage)
}
})
system.result("progress::hide", () => {
hideLoadingBar()
})
system.result("izitoast::error", info => {
iziToast.error(info)
})
system.result("progress::info", info => {
iziToast.error(info)
})
window.onload = () => { window.onload = () => {
system.call("hardware::ramInformation") system.call("hardware::ramInformation")
system.call("game::parseOptions") system.call("game::parseOptions")
@ -140,5 +230,6 @@ window.onload = () => {
system.call("player::profile") system.call("player::profile")
system.call("settings::read") system.call("settings::read")
system.call("oculus::getdefaultshaderset") system.call("oculus::getdefaultshaderset")
hideLoadingBar()
startAudio() startAudio()
} }

28
app/loading.html Normal file
View File

@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="./assets/css/banned.css">
<title>Launcher banni</title>
</head>
<body>
<main>
<button class="close" onclick="system.call('window::close')">
<i class="fas fa-times"></i>
</button>
<article>
<section class="logo">
<img src="./assets/img/icon.png" alt="">
<h2>
Veuillez patienter
</h2>
</section>
</article>
</main>
<script src="./assets/js/banned.js"></script>
</body>
</html>

View File

@ -3,6 +3,7 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/izitoast/1.4.0/css/iziToast.css">
<link rel="stylesheet" href="./assets/css/index.css"> <link rel="stylesheet" href="./assets/css/index.css">
<title>NyanLauncher</title> <title>NyanLauncher</title>
</head> </head>
@ -11,6 +12,9 @@
<button class="close" onclick="system.call('window::close')"> <button class="close" onclick="system.call('window::close')">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
<button class="close" onclick="system.call('window::reduce')">
<i class="fas fa-minus"></i>
</button>
<nav hidden> <nav hidden>
<button class="close" onclick="hideNavBar()"> <button class="close" onclick="hideNavBar()">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
@ -59,7 +63,7 @@
Distance de rendu Distance de rendu
</label> </label>
<div class="ranges"> <div class="ranges">
<span id="renderDistance"> <span id="currentRenderDistance">
4 4
</span> </span>
<input type="range" name="renderDistance" min="4" max="32" id="renderDistance" onchange="handleOptionsChanges(this.name, this.value)"> <input type="range" name="renderDistance" min="4" max="32" id="renderDistance" onchange="handleOptionsChanges(this.name, this.value)">
@ -72,7 +76,7 @@
Taille de l'interface Taille de l'interface
</label> </label>
<div class="ranges"> <div class="ranges">
<span id="guiScale"> <span id="currentGuiScale">
1 1
</span> </span>
<input type="range" name="guiScale" min="1" max="4" id="guiScale" onchange="handleOptionsChanges(this.name, this.value)"> <input type="range" name="guiScale" min="1" max="4" id="guiScale" onchange="handleOptionsChanges(this.name, this.value)">
@ -125,6 +129,12 @@
100% 100%
</label> </label>
</div> </div>
<button class="classic" onclick="toggleMusicVolume(this)" state="0">
<i class="fas fa-volume"></i>
<span>
Couper le son
</span>
</button>
<button class="classic" onclick="toggleMusic(this)" state="0"> <button class="classic" onclick="toggleMusic(this)" state="0">
<i class="fas fa-pause"></i> <i class="fas fa-pause"></i>
<span> <span>
@ -213,8 +223,15 @@
<img class="mascot" src="./assets/img/sulli.png" alt=""> <img class="mascot" src="./assets/img/sulli.png" alt="">
</section> </section>
</footer> </footer>
<div class="loader">
<div class="full">
<div class="loading">
</div>
</div>
</div>
</main> </main>
<script src="https://cdnjs.cloudflare.com/ajax/libs/izitoast/1.4.0/js/iziToast.min.js"></script>
<script src="./assets/js/index.js"></script> <script src="./assets/js/index.js"></script>
</body> </body>

View File

@ -12,6 +12,9 @@
<button class="close" onclick="system.call('window::close')"> <button class="close" onclick="system.call('window::close')">
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
<button class="close" onclick="system.call('window::reduce')">
<i class="fas fa-minus"></i>
</button>
<article class="loginchoice"> <article class="loginchoice">
<div frame="select"> <div frame="select">
<h2> <h2>

View File

@ -1,4 +1,4 @@
const fs = require("node:fs") require("v8-compile-cache")
const path = require("node:path") const path = require("node:path")
const builder = require("electron-builder") const builder = require("electron-builder")

View File

@ -6,7 +6,8 @@
"checkBanStatus": "/api/v1/ban/iam", "checkBanStatus": "/api/v1/ban/iam",
"telemetry": "/api/v1/telemetry", "telemetry": "/api/v1/telemetry",
"gameFiles": "/api/v1/file/game", "gameFiles": "/api/v1/file/game",
"downloadFile": "/api/v1/file/game/download" "downloadFile": "/api/v1/file/game/download",
"countdown": "/api/v1/countdown"
}, },
"websockets": { "websockets": {
"base": { "base": {

153
main.js
View File

@ -15,12 +15,13 @@ const launcher = new Client()
const { io } = require("socket.io-client") const { io } = require("socket.io-client")
const download = require("download") const download = require("download")
const rpc = require("./modules/rpc") const rpc = require("./modules/rpc")
const java = require("./modules/java")
const socket = io({ const socket = io({
host: config.api.websockets.base.host, host: config.api.websockets.base.host,
port: config.api.websockets.base.port port: config.api.websockets.base.port
}) })
let launcherWindow, auth, gamePlayable = false, launchProcess let launcherWindow, defaultWindow, auth, gamePlayable = false, launchProcess
rpc.startRichPresence() rpc.startRichPresence()
@ -108,6 +109,63 @@ async function createLauncherWindow() {
} }
} }
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() { async function checkIfIAmBanned() {
if (net.isOnline()) { if (net.isOnline()) {
try { try {
@ -133,10 +191,31 @@ async function checkIfIAmBanned() {
} }
} }
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(() => { app.whenReady().then(() => {
createLauncherWindow() createDefaultWindow()
app.on("activate", async () => { app.on("activate", async () => {
if (BrowserWindow.getAllWindows().length === 0) await createLauncherWindow() if (BrowserWindow.getAllWindows().length === 0) await createDefaultWindow()
}) })
}) })
@ -160,6 +239,9 @@ ipcMain.on("call", async (event, data) => {
launcherWindow.close() launcherWindow.close()
app.quit() app.quit()
break break
case "window::reduce":
launcherWindow.minimize()
break
case "skin::set": case "skin::set":
const file = await dialog.showOpenDialog(launcherWindow, { const file = await dialog.showOpenDialog(launcherWindow, {
filters: [ filters: [
@ -186,8 +268,18 @@ ipcMain.on("call", async (event, data) => {
if (data.args.trim() != "") { if (data.args.trim() != "") {
auth = Authenticator.getAuth(data.args.username, data.args.password) auth = Authenticator.getAuth(data.args.username, data.args.password)
launcherSettings.set("auth", { token: (await auth).access_token, type: "mojang", clientToken: (await auth).client_token }) launcherSettings.set("auth", { token: (await auth).access_token, type: "mojang", clientToken: (await auth).client_token })
await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${(await auth).uuid}`) if (await checkCoutdown((await auth).uuid)) {
await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) 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 { } else {
dialog.showErrorBox("Erreur", "Le mot de passe n'est pas défini. Les comptes crackés ne sont pas supporté par le launcher.") dialog.showErrorBox("Erreur", "Le mot de passe n'est pas défini. Les comptes crackés ne sont pas supporté par le launcher.")
} }
@ -200,8 +292,18 @@ ipcMain.on("call", async (event, data) => {
auth = token.mclc() auth = token.mclc()
console.log(auth.meta) console.log(auth.meta)
launcherSettings.set("auth", { token: auth.meta, type: "msa", clientToken: auth.client_token }) launcherSettings.set("auth", { token: auth.meta, type: "msa", clientToken: auth.client_token })
launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) if (await checkCoutdown(auth.uuid)) {
fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${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) { } catch (error) {
console.error(error) console.error(error)
if (error == "error.gui.closed") { if (error == "error.gui.closed") {
@ -261,23 +363,32 @@ ipcMain.on("call", async (event, data) => {
launcherWindow.webContents.openDevTools() launcherWindow.webContents.openDevTools()
break break
case "oculus::defaultshaderset": case "oculus::defaultshaderset":
const oculusProperties = properties.parse(fs.readFileSync(path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")).toString()) const filePath = path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")
if (data.args.boolean) { if (fs.existsSync(filePath)) {
properties.setProperty(oculusProperties, "shaderPack", "Sildur%27s+Vibrant+Shaders+v1.50+Medium.zip") writeOculusConfigFile(filePath)
} else { } else {
properties.setProperty(oculusProperties, "shaderPack", "") fs.copyFileSync(path.join(__dirname, "oculus.properties"), filePath)
writeOculusConfigFile(filePath)
} }
fs.writeFileSync(path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties"), properties.stringify(oculusProperties))
break break
case "oculus::getdefaultshaderset": case "oculus::getdefaultshaderset":
const $oculusProperties = properties.parse(fs.readFileSync(path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")).toString()) const $filePath = path.join(app.getPath("appData"), ".catboat", "config", "oculus.properties")
launcherWindow.webContents.send("Response<oculus::getdefaultshaderset>", properties.getProperty($oculusProperties, "shaderPack") == "Sildur%27s+Vibrant+Shaders+v1.50+Medium.zip" ? true : false) 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 break
} }
}) })
async function launchGame(restartGame) { async function launchGame(restartGame) {
launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: false }) let sendToRenderer = false
if (launcherWindow instanceof BrowserWindow) {
sendToRenderer = true
launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: false })
}
const downloadQueue = [] const downloadQueue = []
const remoteFiles = await fileManager.getRemoteFiles() const remoteFiles = await fileManager.getRemoteFiles()
const localFiles = fs.readdirSync(path.join(app.getPath("appData"), ".catboat"), { recursive: true }) const localFiles = fs.readdirSync(path.join(app.getPath("appData"), ".catboat"), { recursive: true })
@ -313,11 +424,13 @@ async function launchGame(restartGame) {
try { try {
await download(url, path.join(app.getPath("appData"), ".catboat", path.dirname(item))) await download(url, path.join(app.getPath("appData"), ".catboat", path.dirname(item)))
if (launcherWindow instanceof BrowserWindow) { if (launcherWindow instanceof BrowserWindow) {
launcherWindow.webContents.send("Response<progress::update>", { type: "landing" }) 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) { } catch (error) {
if (launcherWindow instanceof BrowserWindow) { if (launcherWindow instanceof BrowserWindow) {
launcherWindow.webContents.send("Response<progress::error>", { type: "landing" }) 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()) dialog.showErrorBox("Erreur lors du téléchargement des fichiers", error.toString())
continue continue
@ -332,6 +445,10 @@ async function launchGame(restartGame) {
console.log(log) console.log(log)
}) })
launcher.on("data", (log) => { launcher.on("data", (log) => {
if (sendToRenderer) {
launcherWindow.webContents.send("Response<game::launched>")
sendToRenderer = false
}
console.log(log) console.log(log)
}) })
launcher.on("data", (log) => { launcher.on("data", (log) => {
@ -346,7 +463,7 @@ async function launchGame(restartGame) {
type: "release" type: "release"
}, },
forge: path.join(app.getPath("appData"), ".catboat", "forge-1.16.5.jar"), forge: path.join(app.getPath("appData"), ".catboat", "forge-1.16.5.jar"),
javaPath: "C:\\Program Files\\Java\\jdk-17\\bin\\java.exe", javaPath: await java.getPath(path.join(app.getPath("appData"), ".catboat", "jre")),
memory: { memory: {
min: 512, min: 512,
max: launcherSettings.get("ram").max max: launcherSettings.get("ram").max

View File

@ -1,182 +1,95 @@
const os = require("os") const { execSync } = require("child_process")
const fs = require("fs")
const path = require("path") const path = require("path")
const osmeta = require("./osmeta") const os = require("os")
const { exec } = require("child_process") const download = require("download")
const files = require("./files")
const config = require("../config.json")
const settings = require("./settings")
const { dialog, BrowserWindow, shell } = require("electron")
const AdmZip = require("adm-zip") const AdmZip = require("adm-zip")
function isJavaInstalled() { const JAVA_DOWNLOAD_URL = {
return new Promise((resolve, reject) => { win32: "https://download.oracle.com/java/17/archive/jdk-17.0.12_windows-x64_bin.zip",
try { darwin: "https://download.oracle.com/java/17/archive/jdk-17.0.12_macos-x64_bin.tar.gz",
exec(`"${settings.get("javaPath") == "" ? "java" : settings.get("javaPath")}" -version`, (error, stdout, stderr) => { linux: "https://download.oracle.com/java/17/archive/jdk-17.0.12_linux-aarch64_bin.tar.gz",
if (error) {
reject({
java: false
})
}
const output = `${stdout}\n${stderr}`
const lines = output.split("\n")
const version = lines.filter(line => line.includes("version \""))[0]
try {
let matchRegEx
if (version.includes("_")) {
matchRegEx = /(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)\_(?<update>\d+)/
} else {
matchRegEx = /(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/
}
const versionMatch = version.match(matchRegEx)
const { major, minor, patch, update } = versionMatch.groups
resolve({
java: true,
version: {
major: parseInt(major),
minor: parseInt(minor),
patch: parseInt(patch),
update: update ? parseInt(update) : null
}
})
} catch (error) {
reject({
java: false,
error
})
}
})
} catch (error) {
reject({
java: false,
error
})
}
})
} }
async function showJavaErrorBox() { function checkJavaVersion(requiredVersion = "17") {
const javaErrorBox = await dialog.showMessageBox(BrowserWindow.getFocusedWindow(), { try {
title: "Mauvaise version de Java", const javaVersionOutput = execSync("java -version", {
message: "Mauvaise version de Java", encoding: "utf8",
detail: "La version de java installé sur votre ordinateur n'est pas compatible avec la version de minecraft utilisé", stdio: "pipe"
icon: path.join(__dirname, "code/app/assets/img/java.png"), })
buttons: [ const versionMatch = javaVersionOutput.match(/"(\d+\.\d+)(\.\d+)?_\d+"/)
"Fermer", if (versionMatch) {
"Installer automatiquement java", const installedVersion = versionMatch[1]
"Installer manuellement java", console.log(`Java is installed. Version: ${installedVersion}`)
"Utilise un chemin d'accès personnalié", if (parseFloat(installedVersion) >= parseFloat(requiredVersion)) {
], console.log("Required Java version is already installed.")
cancelId: 0, return true
defaultId: 1
})
return javaErrorBox
}
function installJava(dest) {
return new Promise(async (resolve, reject) => {
const response = await fetch(`${config.apiServicesURL}/api/java`)
const java = await response.json()
const arch = osmeta.arch() == "unknow" ? "x64" : osmeta.arch()
if (java.downloads[os.platform()]) {
if (java.downloads[os.platform()][arch]) {
files.downloadFile(config.apiServicesURL + java.downloads[os.platform()][arch].url, path.join(dest, "java.zip"), (error, file) => {
if (error) throw error
try {
const zip = new AdmZip(path.join(dest, "java.zip"))
zip.extractAllTo(path.join(dest, "java"), true)
files.removeFile(path.join(dest, "java.zip"))
if (os.platform() == "darwin") {
exec(`chmod +x "${path.join(dest, "java", "jre-1.8.0_411.jre", "Contents", "Home", "bin", "java")}"`, (error) => {
if (error) reject(error)
resolve(path.join(dest, "java", "jre-1.8.0_411.jre", "Contents", "Home", "bin", "java"))
})
} else {
exec(`chmod +x "${path.join(dest, "java", "bin", "java")}"`, (error) => {
if (error) reject(error)
resolve(path.join(dest, "java", "bin", "java"))
})
}
} catch (err) {
reject(err)
}
})
} else { } else {
reject(new Error("Java are not supported for your OS arch")) console.log(`Installed Java version (${installedVersion}) is less than required version (${requiredVersion}).`)
return false
} }
} else { } else {
reject(new Error("Java are not supported for your OS")) console.log("Java is installed but version could not be determined.")
return false
} }
}) } catch (error) {
console.log("Java is not installed.")
return false
}
} }
async function main(requiredJavaVersionMinor, dest) { async function downloadAndInstallJava(extractPath = null) {
return new Promise(async (resolve, reject) => { const platform = os.platform()
try { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "java-installer-"))
const java = await isJavaInstalled()
if (java.version.minor < parseInt(requiredJavaVersionMinor)) { const downloadUrl = JAVA_DOWNLOAD_URL[platform]
const javaErrorBox = await showJavaErrorBox() if (!downloadUrl) {
switch (javaErrorBox.response) { console.error("Unsupported platform for Java installation.")
case 0: return
resolve() }
break
case 1: const zipPath = path.join(tempDir, "java-package.zip")
installJava(dest).then(javaPath => { await download(downloadUrl, tempDir, {
resolve(javaPath) filename: "java-package.zip"
})
break
case 2:
shell.openExternal(`https://www.java.com/fr/download/manual.jsp`)
resolve()
break
case 3:
const javaBinPathSelect = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
filters: [
{
name: "java",
}
]
})
if (javaBinPathSelect.canceled) {
showJavaErrorBox()
} else {
resolve(javaBinPathSelect.filePaths[0])
}
break
}
} else {
resolve()
}
} catch (error) {
const javaErrorBox = await showJavaErrorBox()
switch (javaErrorBox.response) {
case 0:
resolve()
break
case 1:
installJava(dest).then(javaPath => {
resolve(javaPath)
})
break
case 2:
shell.openExternal(`https://www.java.com/fr/download/manual.jsp`)
resolve()
break
case 3:
const javaBinPathSelect = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), {
filters: [
{
name: "java",
}
]
})
if (javaBinPathSelect.canceled) {
showJavaErrorBox()
} else {
resolve(javaBinPathSelect.filePaths[0])
}
break
}
}
}) })
const extractionDir = extractPath || tempDir
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractionDir, true)
fs.unlinkSync(zipPath)
} }
module.exports.java = main async function main(customExtractPath) {
const requiredVersion = "17"
const isJavaInstalled = checkJavaVersion(requiredVersion)
if (!isJavaInstalled) {
if (!fs.existsSync(path.join(customExtractPath, "jdk-17.0.12", "bin", os.platform() == "win32" ? "java.exe" : "java"))) {
await downloadAndInstallJava(customExtractPath || path.join(__dirname, "..", "..", "java"))
}
} else {
console.log("No further action is required.")
}
}
async function getPath(customExtractPath) {
const requiredVersion = "17"
const isJavaInstalled = checkJavaVersion(requiredVersion)
if (!isJavaInstalled) {
if (!fs.existsSync(path.join(customExtractPath, "jdk-17.0.12", "bin", os.platform() == "win32" ? "java.exe" : "java"))) {
await main(customExtractPath)
} else {
return path.join(customExtractPath, "jdk-17.0.12", "bin", os.platform() == "win32" ? "java.exe" : "java")
}
} else {
console.log("No further action is required.")
}
}
module.exports = {
main,
getPath
}

View File

@ -3,19 +3,22 @@ const RPC = require("discord-rpc")
class DiscordRPC { class DiscordRPC {
constructor() { constructor() {
readonly: this.activity = { readonly: this.activity = {
details: "Actif dans le launcher", details: "Officiel | Solva x Alcaz",
timestamps : { start: Date.now() },
assets: { assets: {
large_image: "logo", large_image: "rpc_catboat_large",
large_text: "CatBoat Minecraft Launcher" large_text: "CatBoat Launcher",
small_image : "alflamme_comm_legoshi",
small_text: "by TheAlfiTeam",
}, },
buttons: [ buttons: [
{ {
label: "Discord", "label": "Discord",
url: "https://discord.com/invite/catboat" "url": "https://discord.gg/catboat"
}, },
{ {
label: "CatBoat", "label": "Download Launcher",
url: "https://catboat.fr/" "url": "https://catboat.thealfigame.fr"
} }
], ],
instance: true instance: true
@ -30,12 +33,12 @@ class DiscordRPC {
this.client.request("SET_ACTIVITY", { pid: process.pid, activity: this.activity }) this.client.request("SET_ACTIVITY", { pid: process.pid, activity: this.activity })
console.log("The Discord Rich Presence has been set successfully.") console.log("The Discord Rich Presence has been set successfully.")
}) })
this.client.login({ clientId: "1365563093157154868" }).catch(e => { this.client.login({ clientId: "1259291027148115988" }).catch(e => {
console.log(e)
console.log("Silent client detected: the activity status has been disabled.") console.log("Silent client detected: the activity status has been disabled.")
console.log(e)
this.isEnabled = false this.isEnabled = false
}).then(r => this.isEnabled = true) }).then(r => this.isEnabled = true)
.catch(err => console.log) .catch(err => console.log(err))
} }
} }

8
oculus.properties Normal file
View File

@ -0,0 +1,8 @@
#This file stores configuration options for Oculus, such as the currently active shaderpack
#Sun May 11 07:39:36 CEST 2025
colorSpace=SRGB
disableUpdateMessage=false
enableDebugOptions=false
maxShadowRenderDistance=32
shaderPack=Sildurs_Vibrant_Shaders_v1.50_Medium.zip
enableShaders=true

13
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "catboat-launcher", "name": "catboat-launcher",
"version": "0.0.1-alpha", "version": "0.0.9-alpha",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "catboat-launcher", "name": "catboat-launcher",
"version": "0.0.1-alpha", "version": "0.0.9-alpha",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"dependencies": { "dependencies": {
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
@ -14,7 +14,8 @@
"js-java-properties": "^1.0.3", "js-java-properties": "^1.0.3",
"minecraft-launcher-core": "^3.18.2", "minecraft-launcher-core": "^3.18.2",
"msmc": "^5.0.5", "msmc": "^5.0.5",
"socket.io-client": "^4.8.1" "socket.io-client": "^4.8.1",
"v8-compile-cache": "^2.4.0"
}, },
"devDependencies": { "devDependencies": {
"electron": "^35.2.1", "electron": "^35.2.1",
@ -6596,6 +6597,12 @@
"uuid": "dist/bin/uuid" "uuid": "dist/bin/uuid"
} }
}, },
"node_modules/v8-compile-cache": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.4.0.tgz",
"integrity": "sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==",
"license": "MIT"
},
"node_modules/verror": { "node_modules/verror": {
"version": "1.10.1", "version": "1.10.1",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "catboat-launcher", "name": "catboat-launcher",
"version": "0.0.1-alpha", "version": "0.1.0-alpha",
"description": "a simple minecraft launcher for catboat", "description": "a simple minecraft launcher for catboat",
"main": "main.js", "main": "main.js",
"scripts": { "scripts": {
@ -22,12 +22,14 @@
"electron-builder": "^26.0.12", "electron-builder": "^26.0.12",
"electronmon": "^2.0.3" "electronmon": "^2.0.3"
}, },
"homepage": "https://nyancraft.catboat.fr",
"dependencies": { "dependencies": {
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"download": "^8.0.0", "download": "^8.0.0",
"js-java-properties": "^1.0.3", "js-java-properties": "^1.0.3",
"minecraft-launcher-core": "^3.18.2", "minecraft-launcher-core": "^3.18.2",
"msmc": "^5.0.5", "msmc": "^5.0.5",
"socket.io-client": "^4.8.1" "socket.io-client": "^4.8.1",
"v8-compile-cache": "^2.4.0"
} }
} }