Compare commits

...

13 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
1a31e419cc sync 2025-05-10 04:12:32 +02:00
bf9406f924 new commit 2025-05-09 00:00:55 +02:00
4f01013d42 Update main.js 2025-05-06 19:51:48 +02:00
9a6e4ab0d1 commit 2025-05-01 23:35:51 +02:00
22 changed files with 647 additions and 113 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 }}

Binary file not shown.

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;
} }
@ -92,7 +106,7 @@ img.logo {
img.mascot { img.mascot {
width: 50%; width: 50%;
float: right; float: right;
margin-top: calc(300px - (50% + 12px)); margin-top: calc(300px - (50% + 32px));
} }
fieldset { fieldset {
@ -372,7 +386,63 @@ div.checkboxes > input[type="checkbox"] {
font-size: small; font-size: small;
} }
div.loader {
overflow: hidden;
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;
} }
/* 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% {
transform: translateX(-100%);
}
100% {
transform: translateX(200%);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 389 KiB

BIN
app/assets/img/sulli.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 846 KiB

View File

@ -7,7 +7,7 @@ const audioPourcentageLabel = document.querySelector("label[for='audioVolume']")
const audio = new Audio() const audio = new Audio()
function startAudio() { function startAudio() {
audio.src = "./assets/audio/Golden Hill (Radio Edit).mp3" audio.src = "./assets/audio/main_menu.mp3"
audio.loop = true audio.loop = true
audio.play() audio.play()
audio.onended = () => { audio.onended = () => {
@ -58,25 +58,39 @@ 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}%`
} }
function logout() { function logout() {
localStorage.removeItem("user") system.call("auth::reset")
document.location.href = './login.html' document.location.href = './login.html'
} }
@ -84,30 +98,82 @@ system.result("server::ping", pong => {
playersStatus.innerText = `${pong.players.online}/${pong.players.max}` playersStatus.innerText = `${pong.players.online}/${pong.players.max}`
}) })
system.result("player::profile", playerProfile => {
if (!localStorage.getItem("user")) {
localStorage.setItem("user", JSON.stringify(playerProfile))
}
})
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#current${key.replace(/./, c => c.toUpperCase())}`)
if (span) {
span.innerText = Math.floor(value)
}
} }
function handleSettingsChanges(key, value) { function handleSettingsChanges(key, value) {
system.call("settings::set", { key, value }) system.call("settings::set", { key, value })
const span = document.querySelector(`span#${key == "ram" ? "currentRam" : key}`)
if (span) {
span.innerText = key == "ram" ? Math.floor(value.max / 1024) + " G" : 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 => {
@ -118,16 +184,52 @@ 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 => {
console.log(playerProfile)
})
system.result("oculus::getdefaultshaderset", 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")
system.call("server::ping") system.call("server::ping")
system.call("player::profile") system.call("player::profile")
system.call("settings::read") system.call("settings::read")
system.call("oculus::getdefaultshaderset")
hideLoadingBar()
startAudio() startAudio()
} }

View File

@ -19,12 +19,3 @@ system.result("auth::microsoft", () => {
system.result("auth::refresh", () => { system.result("auth::refresh", () => {
selectLoginType("select") selectLoginType("select")
}) })
window.onload = () => {
if (localStorage.getItem("user")) {
system.call("auth::refresh", {
user: JSON.parse(localStorage.getItem("user"))
})
selectLoginType("token")
}
}

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>
@ -46,10 +50,10 @@
Ram alloué Ram alloué
</label> </label>
<div class="ranges"> <div class="ranges">
<span> <span id="currentRam">
0.5GB 0.5GB
</span> </span>
<input type="range" name="ram" min="0" max="2048" id="ram" onchange="handleSettingsChanges(this.name, this.value)"> <input type="range" name="ram" min="0" max="2048" id="ram" onchange="handleSettingsChanges('ram', { max: this.value })">
<span id="maxRam"> <span id="maxRam">
MAX MAX
</span> </span>
@ -59,7 +63,7 @@
Distance de rendu Distance de rendu
</label> </label>
<div class="ranges"> <div class="ranges">
<span> <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> <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)">
@ -100,12 +104,12 @@
Luminsoité max Luminsoité max
</label> </label>
</div> </div>
<!-- <div> <div>
<input type="checkbox" name="sildurs_shader" id="sildurs_shader"> <input type="checkbox" name="sildurs_shader" id="sildurs_shader" onclick="system.call('oculus::defaultshaderset', { boolean: this.checked })">
<label for="sildurs_shader"> <label for="sildurs_shader">
Sildur's Shader Sildur's Shader
</label> </label>
</div> --> </div>
</div> </div>
</article> </article>
</details> </details>
@ -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>
@ -205,16 +215,23 @@
<section class="center"> <section class="center">
<img class="logo" src="./assets/img/logo.png" alt=""> <img class="logo" src="./assets/img/logo.png" alt="">
<br> <br>
<button class="play" name="play" onclick="system.call('game::launch')"> <button class="play load" name="play" onclick="system.call('game::launch')">
Jouer Jouer
</button> </button>
</section> </section>
<section class="right"> <section class="right">
<img class="mascot" src="./assets/img/alcaz_mascote.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")
@ -43,7 +43,7 @@ async function buildApp() {
"!dist", "!dist",
"!.env" "!.env"
], ],
buildNumber: 1, buildNumber: 2,
buildVersion: `${package.version}`, buildVersion: `${package.version}`,
releaseInfo: { releaseInfo: {
releaseDate: getFormattedProductionDate(), releaseDate: getFormattedProductionDate(),

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": {

215
main.js
View File

@ -1,4 +1,5 @@
const { BrowserWindow, app, net, dialog, ipcMain, nativeImage, session, shell } = require("electron") const { BrowserWindow, app, net, dialog, ipcMain, nativeImage, session, shell } = require("electron")
const properties = require("js-java-properties")
const msmc = require("msmc") const msmc = require("msmc")
const os = require("node:os") const os = require("node:os")
const fs = require("node:fs") const fs = require("node:fs")
@ -13,12 +14,16 @@ const fileManager = require("./modules/fileManager")
const launcher = new Client() 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 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()
async function createLauncherWindow() { async function createLauncherWindow() {
gameOptions.initOptions(path.join(app.getPath("appData"), ".catboat", "options.txt")) gameOptions.initOptions(path.join(app.getPath("appData"), ".catboat", "options.txt"))
@ -104,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 {
@ -129,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()
}) })
}) })
@ -156,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: [
@ -181,8 +267,19 @@ ipcMain.on("call", async (event, data) => {
case "auth::mojang": case "auth::mojang":
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)
await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${(await auth).uuid}`) 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 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.")
} }
@ -190,11 +287,23 @@ ipcMain.on("call", async (event, data) => {
case "auth::microsoft": case "auth::microsoft":
const authManager = new msmc.Auth("select_account") const authManager = new msmc.Auth("select_account")
try { try {
const xboxManager = await authManager.launch("raw") const xboxManager = await authManager.launch("electron")
const token = await xboxManager.getMinecraft() const token = await xboxManager.getMinecraft()
auth = token.mclc() auth = token.mclc()
await fetch(`${config.api.base}${config.api.endpoints.telemetry}/${hwid.getHWID()}/${auth.uuid}`) console.log(auth.meta)
await launcherWindow.loadFile(path.join(__dirname, "app", "logged.html")) 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) { } catch (error) {
console.error(error) console.error(error)
if (error == "error.gui.closed") { if (error == "error.gui.closed") {
@ -202,32 +311,11 @@ ipcMain.on("call", async (event, data) => {
} }
} }
break break
case "auth::refresh": case "auth::reset":
const user = data.args.user launcherSettings.set("auth", { token: "", type: "msa", clientToken: "" })
if (user.meta?.type == "msa") { break
try { case "settings::set":
const authManager = new msmc.Auth("none") launcherSettings.set(data.args.key, data.args.value)
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<auth::refresh>")
}
} 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<auth::refresh>")
}
}
break break
case "shell::openExternal": case "shell::openExternal":
shell.openExternal(data.args.url) shell.openExternal(data.args.url)
@ -274,17 +362,39 @@ ipcMain.on("call", async (event, data) => {
case "app::devtools": case "app::devtools":
launcherWindow.webContents.openDevTools() launcherWindow.webContents.openDevTools()
break 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) { async function launchGame(restartGame) {
let sendToRenderer = false
if (launcherWindow instanceof BrowserWindow) {
sendToRenderer = true
launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: false }) 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 })
launcherWindow.setProgressBar(10, { if (launcherWindow instanceof BrowserWindow) {
mode: "indeterminate" launcherWindow.webContents.send("Response<progress::update>", { type: "landing" })
}) }
for (const remoteFile of remoteFiles) { for (const remoteFile of remoteFiles) {
try { try {
const localFile = localFiles.find(file => file === remoteFile) const localFile = localFiles.find(file => file === remoteFile)
@ -313,32 +423,34 @@ async function launchGame(restartGame) {
const url = `${config.api.base}${config.api.endpoints.downloadFile}/${new String(item).replace(/\\/g, "/")}` const url = `${config.api.base}${config.api.endpoints.downloadFile}/${new String(item).replace(/\\/g, "/")}`
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)))
launcherWindow.setProgressBar(((downloadQueue.indexOf(item) + 1) / downloadQueue.length) * 100, { if (launcherWindow instanceof BrowserWindow) {
mode: "normal" 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) {
launcherWindow.setProgressBar(((downloadQueue.indexOf(item) + 1) / downloadQueue.length) * 100, { if (launcherWindow instanceof BrowserWindow) {
mode: "error" 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
} }
} }
launcherWindow.setProgressBar(0, {
mode: "none"
})
dialog.showMessageBox(launcherWindow, {
title: "Téléchargement des fichiers",
message: "Téléchargement fini."
})
gamePlayable = true gamePlayable = true
launcher.on("close", () => { launcher.on("close", () => {
launcherSettings.webContents.send("Response<game::launch>", { disablePlayButton: true }) launcherWindow.webContents.send("Response<game::launch>", { disablePlayButton: true })
gamePlayable = true gamePlayable = true
}) })
launcher.on("debug", (log) => { launcher.on("debug", (log) => {
console.log(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) => { launcher.on("data", (log) => {
console.log(log) console.log(log)
}) })
@ -351,6 +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: 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
@ -368,5 +481,7 @@ socket.on("force-game-update", async () => {
}) })
launchProcess.kill() launchProcess.kill()
await launchGame(true) await launchGame(true)
} else {
await launchGame(false)
} }
}) })

95
modules/java.js Normal file
View File

@ -0,0 +1,95 @@
const { execSync } = require("child_process")
const fs = require("fs")
const path = require("path")
const os = require("os")
const download = require("download")
const AdmZip = require("adm-zip")
const JAVA_DOWNLOAD_URL = {
win32: "https://download.oracle.com/java/17/archive/jdk-17.0.12_windows-x64_bin.zip",
darwin: "https://download.oracle.com/java/17/archive/jdk-17.0.12_macos-x64_bin.tar.gz",
linux: "https://download.oracle.com/java/17/archive/jdk-17.0.12_linux-aarch64_bin.tar.gz",
}
function checkJavaVersion(requiredVersion = "17") {
try {
const javaVersionOutput = execSync("java -version", {
encoding: "utf8",
stdio: "pipe"
})
const versionMatch = javaVersionOutput.match(/"(\d+\.\d+)(\.\d+)?_\d+"/)
if (versionMatch) {
const installedVersion = versionMatch[1]
console.log(`Java is installed. Version: ${installedVersion}`)
if (parseFloat(installedVersion) >= parseFloat(requiredVersion)) {
console.log("Required Java version is already installed.")
return true
} else {
console.log(`Installed Java version (${installedVersion}) is less than required version (${requiredVersion}).`)
return false
}
} else {
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 downloadAndInstallJava(extractPath = null) {
const platform = os.platform()
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "java-installer-"))
const downloadUrl = JAVA_DOWNLOAD_URL[platform]
if (!downloadUrl) {
console.error("Unsupported platform for Java installation.")
return
}
const zipPath = path.join(tempDir, "java-package.zip")
await download(downloadUrl, tempDir, {
filename: "java-package.zip"
})
const extractionDir = extractPath || tempDir
const zip = new AdmZip(zipPath)
zip.extractAllTo(extractionDir, true)
fs.unlinkSync(zipPath)
}
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

@ -5,23 +5,27 @@ let launcherDataPath = path.join(__dirname, ".catboat")
const baseSettings = { const baseSettings = {
ram: { ram: {
max: 1024 max: 2048
},
auth: {
token: "",
type: "msa",
clientToken: ""
} }
} }
function initSettings($launcherDataPath) { function initSettings($launcherDataPath) {
launcherDataPath = $launcherDataPath launcherDataPath = $launcherDataPath
const gameDir = path.parse($launcherDataPath).dir if (!fs.existsSync(launcherDataPath)) {
if (!fs.existsSync(gameDir)) {
try { try {
fs.mkdirSync(gameDir) fs.mkdirSync(launcherDataPath)
} catch (error) { } catch (error) {
throw error throw error
} }
} }
if (!fs.existsSync($launcherDataPath)) { if (!fs.existsSync(path.join(launcherDataPath, "launcher_settings.json"))) {
try { try {
fs.writeFileSync($launcherDataPath, JSON.stringify(baseSettings, null, 4)) fs.writeFileSync(path.join(launcherDataPath, "launcher_settings.json"), JSON.stringify(baseSettings, null, 4))
} catch (error) { } catch (error) {
throw error throw error
} }
@ -37,7 +41,8 @@ function writeSettings(settings) {
fs.writeFileSync(path.join(launcherDataPath, "launcher_settings.json"), JSON.stringify(settings, null, 4)) fs.writeFileSync(path.join(launcherDataPath, "launcher_settings.json"), JSON.stringify(settings, null, 4))
return return
} catch (error) { } catch (error) {
throw error clean()
return writeSettings(settings)
} }
} }
@ -58,8 +63,8 @@ function get(key) {
return value return value
} catch (error) { } catch (error) {
console.error("Erreur lors de l'accès aux paramètres :", error.message) clean()
throw error return get(key)
} }
} }
@ -81,10 +86,10 @@ function set(key, newValue) {
obj[keys[keys.length - 1]] = newValue obj[keys[keys.length - 1]] = newValue
fs.writeFileSync(filePath, JSON.stringify(settings, null, 2)) fs.writeFileSync(filePath, JSON.stringify(settings, null, 4))
} catch (error) { } catch (error) {
console.error("Erreur lors de la mise à jour des paramètres :", error.message) clean()
throw error return set(ke, value)
} }
} }
@ -101,7 +106,8 @@ function readSettings() {
try { try {
return JSON.parse(fs.readFileSync(path.join(launcherDataPath, "launcher_settings.json"))) return JSON.parse(fs.readFileSync(path.join(launcherDataPath, "launcher_settings.json")))
} catch (error) { } catch (error) {
throw error clean()
return readSettings()
} }
} }

27
modules/osmeta.js Normal file
View File

@ -0,0 +1,27 @@
const os = require("os")
function arch() {
switch (os.arch()) {
case "x32":
case "ia32":
case "x86":
case "mips":
case "ppc":
case "s390":
return "x86"
case "x64":
case "arm64":
return "arm64"
case "mipsel":
case "ppc64":
case "riscv64":
case "s390x":
case "loong64":
return "x64"
default:
return "unknown"
}
}
module.exports.arch = arch

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

23
package-lock.json generated
View File

@ -1,19 +1,21 @@
{ {
"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",
"download": "^8.0.0", "download": "^8.0.0",
"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",
@ -4218,6 +4220,15 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/js-java-properties": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/js-java-properties/-/js-java-properties-1.0.3.tgz",
"integrity": "sha512-KFvPPxguCIv4T/Z45tk+eDkD2UiPglaarN8qyrY3RsJDnhz2LMlHp52WFYRYjub5F4SlMKv2u0Z6F/yR1eZ5Jg==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
@ -6586,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,11 +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",
"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"
} }
} }