diff --git a/GUI/index.html b/GUI/index.html index 272dab1..507ca85 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -123,6 +123,26 @@ value="Player" /> +
+ +
+ + +
+
+
+ - +
+ +
+ + + + +
+

+ + Switch between stable release and experimental pre-release versions +

+

+ + Changing branch will download and install a different game version +

+
+
+ +
diff --git a/GUI/js/install.js b/GUI/js/install.js index 86a0ede..304948e 100644 --- a/GUI/js/install.js +++ b/GUI/js/install.js @@ -60,6 +60,18 @@ export async function installGame() { const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; const installPath = installPathInput ? installPathInput.value.trim() : ''; + // Récupérer la branche sélectionnée + const selectedBranchRadio = document.querySelector('input[name="installBranch"]:checked'); + const selectedBranch = selectedBranchRadio ? selectedBranchRadio.value : 'release'; + + console.log(`[Install] Installing game with branch: ${selectedBranch}`); + + // Sauvegarder la branche sélectionnée dans le config + if (window.electronAPI && window.electronAPI.saveVersionBranch) { + await window.electronAPI.saveVersionBranch(selectedBranch); + console.log(`[Install] Branch saved to config: ${selectedBranch}`); + } + if (window.LauncherUI) window.LauncherUI.showProgress(); isDownloading = true; if (installBtn) { @@ -69,7 +81,7 @@ export async function installGame() { try { if (window.electronAPI && window.electronAPI.installGame) { - const result = await window.electronAPI.installGame(playerName, '', installPath); + const result = await window.electronAPI.installGame(playerName, '', installPath, selectedBranch); if (result.success) { const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!'; diff --git a/GUI/js/settings.js b/GUI/js/settings.js index dd383be..0d268b8 100644 --- a/GUI/js/settings.js +++ b/GUI/js/settings.js @@ -3,11 +3,12 @@ let customJavaCheck; let customJavaOptions; let customJavaPath; let browseJavaBtn; -let settingsPlayerName; -let discordRPCCheck; -let closeLauncherCheck; -let gpuPreferenceRadios; - +let settingsPlayerName; +let discordRPCCheck; +let closeLauncherCheck; +let gpuPreferenceRadios; +let gameBranchRadios; + // UUID Management elements let currentUuidDisplay; @@ -161,11 +162,12 @@ function setupSettingsElements() { customJavaOptions = document.getElementById('customJavaOptions'); customJavaPath = document.getElementById('customJavaPath'); browseJavaBtn = document.getElementById('browseJavaBtn'); - settingsPlayerName = document.getElementById('settingsPlayerName'); - discordRPCCheck = document.getElementById('discordRPCCheck'); - closeLauncherCheck = document.getElementById('closeLauncherCheck'); - gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]'); - + settingsPlayerName = document.getElementById('settingsPlayerName'); + discordRPCCheck = document.getElementById('discordRPCCheck'); + closeLauncherCheck = document.getElementById('closeLauncherCheck'); + gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]'); + gameBranchRadios = document.querySelectorAll('input[name="gameBranch"]'); + // UUID Management elements currentUuidDisplay = document.getElementById('currentUuid'); @@ -194,14 +196,14 @@ function setupSettingsElements() { settingsPlayerName.addEventListener('change', savePlayerName); } - if (discordRPCCheck) { - discordRPCCheck.addEventListener('change', saveDiscordRPC); - } - - if (closeLauncherCheck) { - closeLauncherCheck.addEventListener('change', saveCloseLauncher); - } - + if (discordRPCCheck) { + discordRPCCheck.addEventListener('change', saveDiscordRPC); + } + + if (closeLauncherCheck) { + closeLauncherCheck.addEventListener('change', saveCloseLauncher); + } + // UUID event listeners if (copyUuidBtn) { @@ -252,6 +254,12 @@ function setupSettingsElements() { }); }); } + + if (gameBranchRadios) { + gameBranchRadios.forEach(radio => { + radio.addEventListener('change', handleBranchChange); + }); + } } function toggleCustomJava() { @@ -344,43 +352,43 @@ async function saveDiscordRPC() { } } -async function loadDiscordRPC() { - try { - if (window.electronAPI && window.electronAPI.loadDiscordRPC) { - const enabled = await window.electronAPI.loadDiscordRPC(); - if (discordRPCCheck) { - discordRPCCheck.checked = enabled; - } - } - } catch (error) { - console.error('Error loading Discord RPC setting:', error); - } -} - -async function saveCloseLauncher() { - try { - if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) { - const enabled = closeLauncherCheck.checked; - await window.electronAPI.saveCloseLauncher(enabled); - } - } catch (error) { - console.error('Error saving close launcher setting:', error); - } -} - -async function loadCloseLauncher() { - try { - if (window.electronAPI && window.electronAPI.loadCloseLauncher) { - const enabled = await window.electronAPI.loadCloseLauncher(); - if (closeLauncherCheck) { - closeLauncherCheck.checked = enabled; - } - } - } catch (error) { - console.error('Error loading close launcher setting:', error); - } -} - +async function loadDiscordRPC() { + try { + if (window.electronAPI && window.electronAPI.loadDiscordRPC) { + const enabled = await window.electronAPI.loadDiscordRPC(); + if (discordRPCCheck) { + discordRPCCheck.checked = enabled; + } + } + } catch (error) { + console.error('Error loading Discord RPC setting:', error); + } +} + +async function saveCloseLauncher() { + try { + if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) { + const enabled = closeLauncherCheck.checked; + await window.electronAPI.saveCloseLauncher(enabled); + } + } catch (error) { + console.error('Error saving close launcher setting:', error); + } +} + +async function loadCloseLauncher() { + try { + if (window.electronAPI && window.electronAPI.loadCloseLauncher) { + const enabled = await window.electronAPI.loadCloseLauncher(); + if (closeLauncherCheck) { + closeLauncherCheck.checked = enabled; + } + } + } catch (error) { + console.error('Error loading close launcher setting:', error); + } +} + async function savePlayerName() { try { @@ -491,15 +499,16 @@ async function loadGpuPreference() { } } -async function loadAllSettings() { - await loadCustomJavaPath(); - await loadPlayerName(); - await loadCurrentUuid(); - await loadDiscordRPC(); - await loadCloseLauncher(); - await loadGpuPreference(); -} - +async function loadAllSettings() { + await loadCustomJavaPath(); + await loadPlayerName(); + await loadCurrentUuid(); + await loadDiscordRPC(); + await loadCloseLauncher(); + await loadGpuPreference(); + await loadVersionBranch(); +} + async function openGameLocation() { try { @@ -891,4 +900,177 @@ function showNotification(message, type = 'info') { } }, 300); }, 3000); -} \ No newline at end of file +}// Append this to settings.js for branch management + +// === Game Branch Management === +async function handleBranchChange(event) { + const newBranch = event.target.value; + const currentBranch = await loadVersionBranch(); + + if (newBranch === currentBranch) { + return; // No change + } + + // Confirm branch change + const branchName = window.i18n ? + window.i18n.t(`settings.branch${newBranch === 'pre-release' ? 'PreRelease' : 'Release'}`) : + newBranch; + + const message = window.i18n ? + window.i18n.t('settings.branchWarning') : + 'Changing branch will download and install a different game version'; + + showCustomConfirm( + message, + window.i18n ? window.i18n.t('settings.gameBranch') : 'Game Branch', + async () => { + await switchBranch(newBranch); + }, + () => { + // Cancel: revert radio selection + loadVersionBranch().then(branch => { + const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`); + if (radioToCheck) { + radioToCheck.checked = true; + } + }); + } + ); +} + +async function switchBranch(newBranch) { + try { + const switchingMsg = window.i18n ? + window.i18n.t('settings.branchSwitching').replace('{branch}', newBranch) : + `Switching to ${newBranch}...`; + + showNotification(switchingMsg, 'info'); + + // Lock play button + const playButton = document.getElementById('playButton'); + if (playButton) { + playButton.disabled = true; + playButton.classList.add('disabled'); + } + + // Save new branch + await window.electronAPI.saveVersionBranch(newBranch); + + const switchedMsg = window.i18n ? + window.i18n.t('settings.branchSwitched').replace('{branch}', newBranch) : + `Switched to ${newBranch} successfully!`; + + showNotification(switchedMsg, 'success'); + + // Suggest reinstalling + setTimeout(() => { + const branchLabel = newBranch === 'release' ? + (window.i18n ? window.i18n.t('install.releaseVersion') : 'Release') : + (window.i18n ? window.i18n.t('install.preReleaseVersion') : 'Pre-Release'); + + const confirmMsg = window.i18n ? + window.i18n.t('settings.branchInstallConfirm').replace('{branch}', branchLabel) : + `The game will be installed for the ${branchLabel} branch. Continue?`; + + showCustomConfirm( + confirmMsg, + window.i18n ? window.i18n.t('settings.installRequired') : 'Installation Required', + async () => { + // Show progress and trigger game installation + if (window.LauncherUI) { + window.LauncherUI.showProgress(); + } + + try { + const playerName = await window.electronAPI.loadUsername(); + const result = await window.electronAPI.installGame(playerName || 'Player', '', '', newBranch); + + if (result.success) { + const successMsg = window.i18n ? + window.i18n.t('progress.installationComplete') : + 'Installation completed successfully!'; + + showNotification(successMsg, 'success'); + + setTimeout(() => { + if (window.LauncherUI) { + window.LauncherUI.hideProgress(); + } + + // Unlock play button + const playButton = document.getElementById('playButton'); + if (playButton) { + playButton.disabled = false; + playButton.classList.remove('disabled'); + } + }, 2000); + } else { + throw new Error(result.error || 'Installation failed'); + } + } catch (error) { + console.error('Installation error:', error); + const errorMsg = window.i18n ? + window.i18n.t('progress.installationFailed').replace('{error}', error.message) : + `Installation failed: ${error.message}`; + + showNotification(errorMsg, 'error'); + + if (window.LauncherUI) { + window.LauncherUI.hideProgress(); + } + + // Unlock play button + const playButton = document.getElementById('playButton'); + if (playButton) { + playButton.disabled = false; + playButton.classList.remove('disabled'); + } + } + }, + () => { + // Cancel - unlock play button + const playButton = document.getElementById('playButton'); + if (playButton) { + playButton.disabled = false; + playButton.classList.remove('disabled'); + } + }, + window.i18n ? window.i18n.t('common.install') : 'Install', + window.i18n ? window.i18n.t('common.cancel') : 'Cancel' + ); + }, 500); + + } catch (error) { + console.error('Error switching branch:', error); + showNotification(`Failed to switch branch: ${error.message}`, 'error'); + + // Revert radio selection + loadVersionBranch().then(branch => { + const radioToCheck = document.querySelector(`input[name="gameBranch"][value="${branch}"]`); + if (radioToCheck) { + radioToCheck.checked = true; + } + }); + } +} + +async function loadVersionBranch() { + try { + if (window.electronAPI && window.electronAPI.loadVersionBranch) { + const branch = await window.electronAPI.loadVersionBranch(); + + // Update radio buttons + if (gameBranchRadios) { + gameBranchRadios.forEach(radio => { + radio.checked = radio.value === branch; + }); + } + + return branch; + } + return 'release'; // Default + } catch (error) { + console.error('Error loading version branch:', error); + return 'release'; + } +} diff --git a/GUI/js/ui.js b/GUI/js/ui.js index 6768f95..bc2b35f 100644 --- a/GUI/js/ui.js +++ b/GUI/js/ui.js @@ -501,6 +501,7 @@ function setupUI() { setupAnimations(); setupFirstLaunchHandlers(); loadLauncherVersion(); + checkGameInstallation(); document.body.focus(); } @@ -520,6 +521,50 @@ async function loadLauncherVersion() { } } +// Check game installation status on startup +async function checkGameInstallation() { + try { + console.log('Checking game installation status...'); + + // Check if game is installed + const isInstalled = await window.electronAPI.isGameInstalled(); + + // Load version_client from config + let versionClient = null; + if (window.electronAPI.loadVersionClient) { + versionClient = await window.electronAPI.loadVersionClient(); + } + + console.log(`Game installed: ${isInstalled}, version_client: ${versionClient}`); + + // If version_client is null and game is not installed, trigger installation + if (versionClient === null && !isInstalled) { + console.log('Game not installed and version_client is null, showing install page...'); + + // Show installation page + const installPage = document.getElementById('install-page'); + const launcher = document.getElementById('launcher-container'); + const sidebar = document.querySelector('.sidebar'); + + if (installPage) { + installPage.style.display = 'block'; + if (launcher) launcher.style.display = 'none'; + if (sidebar) sidebar.style.pointerEvents = 'none'; + + // Unlock play button since we're in install mode + lockPlayButton(false); + } + } else { + // Game is installed or version is set, unlock play button + lockPlayButton(false); + } + } catch (error) { + console.error('Error checking game installation:', error); + // Unlock on error to prevent permanent lock + lockPlayButton(false); + } +} + window.LauncherUI = { showPage, setActiveNav, diff --git a/GUI/locales/en.json b/GUI/locales/en.json index d142831..cb37f84 100644 --- a/GUI/locales/en.json +++ b/GUI/locales/en.json @@ -15,6 +15,9 @@ "title": "FREE TO PLAY LAUNCHER", "playerName": "Player Name", "playerNamePlaceholder": "Enter your name", + "gameBranch": "Game Version", + "releaseVersion": "Release (Stable)", + "preReleaseVersion": "Pre-Release (Experimental)", "customInstallation": "Custom Installation", "installationFolder": "Installation Folder", "pathPlaceholder": "Default location", @@ -125,7 +128,16 @@ "logsLoading": "Loading logs...", "closeLauncher": "Launcher Behavior", "closeOnStart": "Close Launcher on game start", - "closeOnStartDescription": "Automatically close the launcher after Hytale has launched" + "closeOnStartDescription": "Automatically close the launcher after Hytale has launched", + "gameBranch": "Game Branch", + "branchRelease": "Release", + "branchPreRelease": "Pre-Release", + "branchHint": "Switch between stable release and experimental pre-release versions", + "branchWarning": "Changing branch will download and install a different game version", + "branchSwitching": "Switching to {branch}...", + "branchSwitched": "Switched to {branch} successfully!", + "installRequired": "Installation Required", + "branchInstallConfirm": "The game will be installed for the {branch} branch. Continue?" }, "uuid": { "modalTitle": "UUID Management", @@ -157,7 +169,8 @@ "delete": "Delete", "edit": "Edit", "loading": "Loading...", - "apply": "Apply" + "apply": "Apply", + "install": "Install" }, "notifications": { "gameDataNotFound": "Error: Game data not found", diff --git a/GUI/locales/es.json b/GUI/locales/es.json index 4bb89c8..72f1c13 100644 --- a/GUI/locales/es.json +++ b/GUI/locales/es.json @@ -14,8 +14,9 @@ "install": { "title": "LAUNCHER GRATUITO", "playerName": "Nombre del Jugador", - "playerNamePlaceholder": "Ingresa tu nombre", - "customInstallation": "Instalación Personalizada", + "playerNamePlaceholder": "Ingresa tu nombre", "gameBranch": "Versión del Juego", + "releaseVersion": "Lanzamiento (Estable)", + "preReleaseVersion": "Pre-Lanzamiento (Experimental)", "customInstallation": "Instalación Personalizada", "installationFolder": "Carpeta de Instalación", "pathPlaceholder": "Ubicación predeterminada", "browse": "Examinar", @@ -125,7 +126,16 @@ "logsLoading": "Cargando registros...", "closeLauncher": "Comportamiento del Launcher", "closeOnStart": "Cerrar Launcher al iniciar el juego", - "closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado" + "closeOnStartDescription": "Cierra automáticamente el launcher después de que Hytale se haya iniciado", + "gameBranch": "Rama del Juego", + "branchRelease": "Lanzamiento", + "branchPreRelease": "Pre-Lanzamiento", + "branchHint": "Cambia entre la versión estable y la versión experimental de pre-lanzamiento", + "branchWarning": "Cambiar de rama descargará e instalará una versión diferente del juego", + "branchSwitching": "Cambiando a {branch}...", + "branchSwitched": "¡Cambiado a {branch} con éxito!", + "installRequired": "Instalación Requerida", + "branchInstallConfirm": "El juego se instalará para la rama {branch}. ¿Continuar?" }, "uuid": { "modalTitle": "Gestión de UUID", @@ -157,7 +167,8 @@ "delete": "Eliminar", "edit": "Editar", "loading": "Cargando...", - "apply": "Aplicar" + "apply": "Aplicar", + "install": "Instalar" }, "notifications": { "gameDataNotFound": "Error: No se encontraron datos del juego", diff --git a/GUI/locales/pt-BR.json b/GUI/locales/pt-BR.json index 492440b..12e31f7 100644 --- a/GUI/locales/pt-BR.json +++ b/GUI/locales/pt-BR.json @@ -14,8 +14,9 @@ "install": { "title": "LANÇADOR JOGO GRATUITO", "playerName": "Nome do Jogador", - "playerNamePlaceholder": "Digite seu nome", - "customInstallation": "Instalação Personalizada", + "playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo", + "releaseVersion": "Lançamento (Estável)", + "preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada", "installationFolder": "Pasta de Instalação", "pathPlaceholder": "Local padrão", "browse": "Procurar", @@ -125,7 +126,16 @@ "logsLoading": "Carregando registros...", "closeLauncher": "Comportamento do Lançador", "closeOnStart": "Fechar Lançador ao iniciar o jogo", - "closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado" + "closeOnStartDescription": "Fechar automaticamente o lançador após o Hytale ter sido iniciado", + "gameBranch": "Versão do Jogo", + "branchRelease": "Lançamento", + "branchPreRelease": "Pré-Lançamento", + "branchHint": "Alterne entre a versão estável e a versão experimental de pré-lançamento", + "branchWarning": "Mudar de versão irá baixar e instalar uma versão diferente do jogo", + "branchSwitching": "Mudando para {branch}...", + "branchSwitched": "Mudado para {branch} com sucesso!", + "installRequired": "Instalação Necessária", + "branchInstallConfirm": "O jogo será instalado para o ramo {branch}. Continuar?" }, "uuid": { "modalTitle": "Gerenciamento de UUID", @@ -158,7 +168,8 @@ "delete": "Excluir", "edit": "Editar", "loading": "Carregando...", - "apply": "Aplicar" + "apply": "Aplicar", + "install": "Instalar" }, "notifications": { "gameDataNotFound": "Erro: Dados do jogo não encontrados", diff --git a/GUI/style.css b/GUI/style.css index 10967ff..99700dd 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -662,6 +662,57 @@ body { box-shadow: none; } +/* Radio buttons for install page */ +.radio-group { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.radio-label { + display: flex; + align-items: center; + padding: 1rem; + background: rgba(255, 255, 255, 0.05); + border: 2px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + cursor: pointer; + transition: all 0.3s ease; +} + +.radio-label:hover { + background: rgba(255, 255, 255, 0.08); + border-color: rgba(147, 51, 234, 0.5); +} + +.radio-label .custom-radio { + position: absolute; + opacity: 0; + cursor: pointer; +} + +.radio-label .custom-radio:checked ~ .radio-text { + color: #9333ea; +} + +.radio-label:has(.custom-radio:checked) { + background: rgba(147, 51, 234, 0.15); + border-color: #9333ea; + box-shadow: 0 0 20px rgba(147, 51, 234, 0.2); +} + +.radio-text { + display: flex; + align-items: center; + color: #d1d5db; + font-weight: 500; + transition: color 0.3s ease; +} + +.radio-text i { + margin-right: 0.5rem; +} + .launcher-container { flex: 1; display: flex; diff --git a/backend/core/config.js b/backend/core/config.js index 03cff49..16f039c 100644 --- a/backend/core/config.js +++ b/backend/core/config.js @@ -304,6 +304,30 @@ function loadGpuPreference() { return config.gpuPreference || 'auto'; } +function saveVersionClient(versionClient) { + saveConfig({ version_client: versionClient }); +} + +function loadVersionClient() { + const config = loadConfig(); + return config.version_client !== undefined ? config.version_client : null; +} + +function saveVersionBranch(versionBranch) { + const branch = versionBranch || 'release'; + if (branch !== 'release' && branch !== 'pre-release') { + console.warn(`Invalid branch "${branch}", defaulting to "release"`); + saveConfig({ version_branch: 'release' }); + } else { + saveConfig({ version_branch: branch }); + } +} + +function loadVersionBranch() { + const config = loadConfig(); + return config.version_branch || 'release'; +} + module.exports = { loadConfig, saveConfig, @@ -343,5 +367,10 @@ module.exports = { loadGpuPreference, // Close Launcher export saveCloseLauncherOnStart, - loadCloseLauncherOnStart + loadCloseLauncherOnStart, + // Version Management exports + saveVersionClient, + loadVersionClient, + saveVersionBranch, + loadVersionBranch }; diff --git a/backend/launcher.js b/backend/launcher.js index 32a6c59..fbe424e 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -33,7 +33,12 @@ const { resetCurrentUserUuid, // GPU Preference saveGpuPreference, - loadGpuPreference + loadGpuPreference, + // Version Management + saveVersionClient, + loadVersionClient, + saveVersionBranch, + loadVersionBranch } = require('./core/config'); const { getResolvedAppDir, getModsPath } = require('./core/paths'); @@ -138,6 +143,10 @@ module.exports = { // Version functions getInstalledClientVersion, getLatestClientVersion, + saveVersionClient, + loadVersionClient, + saveVersionBranch, + loadVersionBranch, // News functions getHytaleNews, diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 19d1c20..5243823 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -7,7 +7,7 @@ const { spawn } = require('child_process'); const { v4: uuidv4 } = require('uuid'); const { getResolvedAppDir, findClientPath } = require('../core/paths'); const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils'); -const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain } = require('../core/config'); +const { saveUsername, saveInstallPath, loadJavaPath, getUuidForUser, getAuthServerUrl, getAuthDomain, loadVersionBranch, loadVersionClient, saveVersionClient } = require('../core/config'); const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager'); const { updateGameFiles } = require('./gameManager'); @@ -101,10 +101,11 @@ function generateLocalTokens(uuid, name) { }; } -async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') { +async function launchGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) { + const branch = branchOverride || loadVersionBranch(); const customAppDir = getResolvedAppDir(installPathOverride); - const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); - const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest'); + const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); + const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest'); const userDataDir = path.join(customGameDir, 'Client', 'UserData'); const gameLatest = customGameDir; @@ -151,13 +152,14 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName); // Patch client and server binaries to use custom auth server (BEFORE signing on macOS) + // FORCE patch on every launch to ensure consistency const authDomain = getAuthDomain(); if (clientPatcher) { try { if (progressCallback) { progressCallback('Patching game for custom server...', null, null, null, null); } - console.log(`Patching game binaries for ${authDomain}...`); + console.log(`Force patching game binaries for ${authDomain}...`); const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => { console.log(`[Patcher] ${msg}`); @@ -167,16 +169,12 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr }); if (patchResult.success) { - if (patchResult.alreadyPatched) { - console.log(`Game already patched for ${authDomain}`); - } else { - console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`); - if (patchResult.client) { - console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`); - } - if (patchResult.server) { - console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`); - } + console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`); + if (patchResult.client) { + console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`); + } + if (patchResult.server) { + console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`); } } else { console.warn('Game patching failed:', patchResult.error); @@ -355,23 +353,23 @@ exec "$REAL_JAVA" "\${ARGS[@]}" } } -async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto') { +async function launchGameWithVersionCheck(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, gpuPreference = 'auto', branchOverride = null) { try { + const branch = branchOverride || loadVersionBranch(); + if (progressCallback) { progressCallback('Checking for updates...', 0, null, null, null); } - const [installedVersion, latestVersion] = await Promise.all([ - getInstalledClientVersion(), - getLatestClientVersion() - ]); + const installedVersion = loadVersionClient(); + const latestVersion = await getLatestClientVersion(branch); - console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion}`); + console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion} (branch: ${branch})`); let needsUpdate = false; - if (installedVersion && latestVersion && installedVersion !== latestVersion) { + if (!installedVersion || installedVersion !== latestVersion) { needsUpdate = true; - console.log('Version mismatch detected, update required'); + console.log('Version mismatch or not installed, update required'); } if (needsUpdate) { @@ -380,13 +378,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac } const customAppDir = getResolvedAppDir(installPathOverride); - const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); + const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); const customToolsDir = path.join(customAppDir, 'butler'); const customCacheDir = path.join(customAppDir, 'cache'); try { - await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir); - console.log('Game updated successfully, waiting before launch...'); + await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch); + console.log('Game updated successfully, patching will be forced on launch...'); if (progressCallback) { progressCallback('Preparing game launch...', 90, null, null, null); @@ -406,7 +404,7 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac progressCallback('Launching game...', 80, null, null, null); } - return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference); + return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch); } catch (error) { console.error('Error in version check and launch:', error); if (progressCallback) { diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 7d4245a..1785461 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -7,10 +7,11 @@ const { downloadFile } = require('../utils/fileManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { installButler } = require('./butlerManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); -const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig } = require('../core/config'); +const { saveUsername, saveInstallPath, loadJavaPath, CONFIG_FILE, loadConfig, loadVersionBranch, saveVersionClient, loadVersionClient } = require('../core/config'); const { resolveJavaPath, detectSystemJava, downloadJRE, getJavaExec, getBundledJavaPath } = require('./javaManager'); +const userDataBackup = require('../utils/userDataBackup'); -async function downloadPWR(version = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) { +async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) { const osName = getOS(); const arch = getArch(); @@ -18,9 +19,9 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.'); } - const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`; + const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}`; - const dest = path.join(cacheDir, fileName); + const dest = path.join(cacheDir, `${branch}_${fileName}`); if (fs.existsSync(dest)) { console.log('PWR file found in cache:', dest); @@ -104,13 +105,22 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir console.log('Installation complete'); } -async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR) { +async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, cacheDir = CACHE_DIR, branchOverride = null) { let tempUpdateDir; + let backupPath = null; + const branch = branchOverride || loadVersionBranch(); + const installPath = path.dirname(path.dirname(path.dirname(path.dirname(gameDir)))); + + // Vérifier si on a version_client et version_branch dans config.json + const config = loadConfig(); + const hasVersionConfig = !!(config.version_client && config.version_branch); + console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`); + try { if (progressCallback) { progressCallback('Updating game files...', 0, null, null, null); } - console.log(`Updating game files to version: ${newVersion}`); + console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`); tempUpdateDir = path.join(gameDir, '..', 'temp_update'); @@ -123,7 +133,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, progressCallback('Downloading new game version...', 10, null, null, null); } - const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir); + const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir); if (progressCallback) { progressCallback('Extracting new files...', 50, null, null, null); @@ -132,34 +142,18 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir); if (progressCallback) { - progressCallback('Replacing game files...', 80, null, null, null); + progressCallback('Backing up user data...', 70, null, null, null); } - let userDataBackup = null; - const userDataPath = findUserDataRecursive(gameDir); + // Backup UserData using new system + try { + backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig); + } catch (backupError) { + console.warn('UserData backup failed:', backupError.message); + } - if (userDataPath && fs.existsSync(userDataPath)) { - userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now()); - console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`); - - function copyRecursive(src, dest) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true }); - } - const files = fs.readdirSync(src); - for (const file of files) { - copyRecursive(path.join(src, file), path.join(dest, file)); - } - } else { - fs.copyFileSync(src, dest); - } - } - - copyRecursive(userDataPath, userDataBackup); - } else { - console.log('No UserData folder found in game directory'); + if (progressCallback) { + progressCallback('Replacing game files...', 80, null, null, null); } if (fs.existsSync(gameDir)) { @@ -175,44 +169,26 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback); console.log('Logo@2x.png update result after update:', logoResult); - if (userDataBackup && fs.existsSync(userDataBackup)) { - const newUserDataPath = findUserDataPath(gameDir); - const userDataParent = path.dirname(newUserDataPath); + if (progressCallback) { + progressCallback('Restoring user data...', 90, null, null, null); + } - if (!fs.existsSync(userDataParent)) { - fs.mkdirSync(userDataParent, { recursive: true }); + // Restore UserData using new system + if (backupPath) { + try { + await userDataBackup.restoreUserData(backupPath, installPath, branch); + await userDataBackup.cleanupBackup(backupPath); + } catch (restoreError) { + console.warn('UserData restore failed:', restoreError.message); } - - console.log(`Restoring UserData to: ${newUserDataPath}`); - - function copyRecursive(src, dest) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) { - fs.mkdirSync(dest, { recursive: true }); - } - const files = fs.readdirSync(src); - for (const file of files) { - copyRecursive(path.join(src, file), path.join(dest, file)); - } - } else { - fs.copyFileSync(src, dest); - } - } - - copyRecursive(userDataBackup, newUserDataPath); } console.log(`Game files updated successfully to version: ${newVersion}`); - - if (userDataBackup && fs.existsSync(userDataBackup)) { - try { - fs.rmSync(userDataBackup, { recursive: true, force: true }); - console.log('UserData backup cleaned up'); - } catch (cleanupError) { - console.warn('Could not clean up UserData backup:', cleanupError.message); - } - } + + // Save the updated version and branch to config + saveVersionClient(newVersion); + const { saveVersionBranch } = require('../core/config'); + saveVersionBranch(branch); console.log('Waiting for file system sync...'); await new Promise(resolve => setTimeout(resolve, 2000)); @@ -225,9 +201,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, } catch (error) { console.error('Error updating game files:', error); - if (userDataBackup && fs.existsSync(userDataBackup)) { + if (backupPath) { try { - fs.rmSync(userDataBackup, { recursive: true, force: true }); + await userDataBackup.cleanupBackup(backupPath); console.log('UserData backup cleaned up after error'); } catch (cleanupError) { console.warn('Could not clean up UserData backup:', cleanupError.message); @@ -242,21 +218,45 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, } } -function isGameInstalled() { +function isGameInstalled(branchOverride = null) { + const branch = branchOverride || loadVersionBranch(); const appDir = getResolvedAppDir(); - const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); + const gameDir = path.join(appDir, branch, 'package', 'game', 'latest'); const clientPath = findClientPath(gameDir); return clientPath !== null; } -async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride) { +async function installGame(playerName = 'Player', progressCallback, javaPathOverride, installPathOverride, branchOverride = null) { + const branch = branchOverride || loadVersionBranch(); const customAppDir = getResolvedAppDir(installPathOverride); const customCacheDir = path.join(customAppDir, 'cache'); const customToolsDir = path.join(customAppDir, 'butler'); - const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); - const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest'); + const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest'); + const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest'); const userDataDir = path.join(customGameDir, 'Client', 'UserData'); + // Vérifier si on a version_client et version_branch dans config.json + const config = loadConfig(); + const hasVersionConfig = !!(config.version_client && config.version_branch); + console.log(`[InstallGame] Configuration détectée - version_client: ${config.version_client}, version_branch: ${config.version_branch}`); + console.log(`[InstallGame] hasVersionConfig: ${hasVersionConfig}`); + + // Backup UserData AVANT l'installation si nécessaire + let backupPath = null; + if (progressCallback) { + progressCallback('Checking for existing UserData...', 5, null, null, null); + } + + try { + console.log(`[InstallGame] Tentative de backup UserData (hasVersionConfig: ${hasVersionConfig})...`); + backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig); + if (backupPath) { + console.log(`[InstallGame] ✓ UserData sauvegardé dans: ${backupPath}`); + } + } catch (backupError) { + console.warn('[InstallGame] ✗ Backup UserData échoué:', backupError.message); + } + [customAppDir, customCacheDir, customToolsDir].forEach(dir => { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); @@ -313,18 +313,47 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver if (progressCallback) { progressCallback('Fetching game files...', null, null, null, null); } - console.log('Installing game files...'); + console.log(`Installing game files for branch: ${branch}...`); - const latestVersion = await getLatestClientVersion(); - const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); + const latestVersion = await getLatestClientVersion(branch); + const pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); + // Save the installed version and branch to config + saveVersionClient(latestVersion); + const { saveVersionBranch } = require('../core/config'); + saveVersionBranch(branch); + const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback); console.log('HomePage.ui update result after installation:', homeUIResult); const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback); console.log('Logo@2x.png update result after installation:', logoResult); + // Ensure UserData directory exists + if (!fs.existsSync(userDataDir)) { + console.log(`[InstallGame] Création du dossier UserData dans: ${userDataDir}`); + fs.mkdirSync(userDataDir, { recursive: true }); + } + + // Restore UserData from backup if exists + if (backupPath) { + if (progressCallback) { + progressCallback('Restoring UserData...', 95, null, null, null); + } + + try { + console.log(`[InstallGame] Restauration du UserData depuis: ${backupPath}`); + await userDataBackup.restoreUserData(backupPath, customAppDir, branch); + await userDataBackup.cleanupBackup(backupPath); + console.log('[InstallGame] ✓ UserData restauré avec succès'); + } catch (restoreError) { + console.warn('[InstallGame] ✗ Erreur lors de la restauration UserData:', restoreError.message); + } + } else { + console.log('[InstallGame] Aucun backup à restaurer, dossier UserData vide créé'); + } + if (progressCallback) { progressCallback('Installation complete', 100, null, null, null); } @@ -357,8 +386,9 @@ async function uninstallGame() { } } -function checkExistingGameInstallation() { +function checkExistingGameInstallation(branchOverride = null) { try { + const branch = branchOverride || loadVersionBranch(); const config = loadConfig(); if (!config.installPath || !config.installPath.trim()) { @@ -366,7 +396,7 @@ function checkExistingGameInstallation() { } const installPath = config.installPath.trim(); - const gameDir = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest'); + const gameDir = path.join(installPath, 'HytaleF2P', branch, 'package', 'game', 'latest'); if (!fs.existsSync(gameDir)) { return null; @@ -384,7 +414,8 @@ function checkExistingGameInstallation() { clientPath: clientPath, userDataPath: userDataPath, installPath: installPath, - hasUserData: userDataPath && fs.existsSync(userDataPath) + hasUserData: userDataPath && fs.existsSync(userDataPath), + branch: branch }; } catch (error) { console.error('Error checking existing game installation:', error); @@ -392,40 +423,32 @@ function checkExistingGameInstallation() { } } -async function repairGame(progressCallback) { +async function repairGame(progressCallback, branchOverride = null) { + const branch = branchOverride || loadVersionBranch(); const appDir = getResolvedAppDir(); - const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); + const gameDir = path.join(appDir, branch, 'package', 'game', 'latest'); + const installPath = appDir; + let backupPath = null; + + // Vérifier si on a version_client et version_branch dans config.json + const config = loadConfig(); + const hasVersionConfig = !!(config.version_client && config.version_branch); + console.log(`[RepairGame] hasVersionConfig: ${hasVersionConfig}`); // Check if game exists if (!fs.existsSync(gameDir)) { throw new Error('Game directory not found. Cannot repair.'); } - // Locate UserData - const userDataPath = findUserDataRecursive(gameDir); - let userDataBackup = null; - if (progressCallback) { progressCallback('Backing up user data...', 10, null, null, null); } - // Backup UserData - if (userDataPath && fs.existsSync(userDataPath)) { - userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now()); - console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`); - - // Copy function - function copyRecursive(src, dest) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child))); - } else { - fs.copyFileSync(src, dest); - } - } - - copyRecursive(userDataPath, userDataBackup); + // Backup UserData using new system + try { + backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig); + } catch (backupError) { + console.warn('UserData backup failed during repair:', backupError.message); } if (progressCallback) { @@ -446,39 +469,21 @@ async function repairGame(progressCallback) { // Passing null/undefined for overrides to use defaults/saved configs // installGame calls progressCallback internally - await installGame('Player', progressCallback); + await installGame('Player', progressCallback, null, null, branch); - // Restore UserData - if (userDataBackup && fs.existsSync(userDataBackup)) { + // Restore UserData using new system + if (backupPath) { if (progressCallback) { progressCallback('Restoring user data...', 90, null, null, null); } - // installGame creates: path.join(customGameDir, 'Client', 'UserData') - const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); - const newUserDataPath = path.join(newGameDir, 'Client', 'UserData'); - - if (!fs.existsSync(newUserDataPath)) { - fs.mkdirSync(newUserDataPath, { recursive: true }); + try { + await userDataBackup.restoreUserData(backupPath, installPath, branch); + await userDataBackup.cleanupBackup(backupPath); + console.log('UserData restored successfully after repair'); + } catch (restoreError) { + console.warn('UserData restore failed after repair:', restoreError.message); } - - console.log(`Restoring UserData to ${newUserDataPath}`); - - function copyRecursive(src, dest) { - const stat = fs.statSync(src); - if (stat.isDirectory()) { - if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); - fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child))); - } else { - fs.copyFileSync(src, dest); - } - } - - copyRecursive(userDataBackup, newUserDataPath); - - // Cleanup Backup - console.log('Cleaning up repair backup...'); - fs.rmSync(userDataBackup, { recursive: true, force: true }); } if (progressCallback) { diff --git a/backend/services/firstLaunch.js b/backend/services/firstLaunch.js index 103b04f..5cd78dd 100644 --- a/backend/services/firstLaunch.js +++ b/backend/services/firstLaunch.js @@ -1,6 +1,6 @@ const path = require('path'); const fs = require('fs'); -const { markAsLaunched, loadConfig } = require('../core/config'); +const { markAsLaunched, loadConfig, saveVersionBranch, saveVersionClient, loadVersionBranch, loadVersionClient } = require('../core/config'); const { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager'); @@ -56,6 +56,20 @@ async function handleFirstLaunchCheck(progressCallback) { try { const config = loadConfig(); + // Initialize version_branch and version_client if not set + const currentBranch = loadVersionBranch(); + const currentVersion = loadVersionClient(); + + if (!currentBranch) { + console.log('Initializing version_branch to "release"'); + saveVersionBranch('release'); + } + + if (currentVersion === undefined || currentVersion === null) { + console.log('Initializing version_client to null (will trigger installation)'); + saveVersionClient(null); + } + if (config.hasLaunchedBefore === true) { return { isFirstLaunch: false, needsUpdate: false }; } diff --git a/backend/services/versionManager.js b/backend/services/versionManager.js index cf7b9ba..56bfa96 100644 --- a/backend/services/versionManager.js +++ b/backend/services/versionManager.js @@ -1,9 +1,10 @@ const axios = require('axios'); -async function getLatestClientVersion() { +async function getLatestClientVersion(branch = 'release') { try { - console.log('Fetching latest client version from API...'); + console.log(`Fetching latest client version from API (branch: ${branch})...`); const response = await axios.get('https://files.hytalef2p.com/api/version_client', { + params: { branch }, timeout: 5000, headers: { 'User-Agent': 'Hytale-F2P-Launcher' @@ -12,7 +13,7 @@ async function getLatestClientVersion() { if (response.data && response.data.client_version) { const version = response.data.client_version; - console.log(`Latest client version: ${version}`); + console.log(`Latest client version for ${branch}: ${version}`); return version; } else { console.log('Warning: Invalid API response, falling back to default version'); diff --git a/backend/updateManager.js b/backend/updateManager.js index fea0f0f..e7894e9 100644 --- a/backend/updateManager.js +++ b/backend/updateManager.js @@ -11,6 +11,18 @@ class UpdateManager { } async checkForUpdates() { + // Disabled: Using electron-updater for automatic updates instead + console.log('Update check skipped - using electron-updater'); + console.log(`Current version: ${CURRENT_VERSION}`); + + return { + updateAvailable: false, + currentVersion: CURRENT_VERSION, + newVersion: CURRENT_VERSION, + message: 'Using electron-updater for automatic updates' + }; + + /* kept for reference try { console.log('Checking for updates...'); console.log(`Local version: ${CURRENT_VERSION}`); @@ -54,6 +66,7 @@ class UpdateManager { currentVersion: CURRENT_VERSION }; } + */ } getDownloadUrl() { diff --git a/backend/utils/userDataBackup.js b/backend/utils/userDataBackup.js new file mode 100644 index 0000000..df1b69b --- /dev/null +++ b/backend/utils/userDataBackup.js @@ -0,0 +1,129 @@ +const fs = require('fs-extra'); +const path = require('path'); + +/** + * Backup and restore UserData folder during game updates + */ +class UserDataBackup { + /** + * Backup UserData folder to a temporary location + * @param {string} installPath - Base installation path (e.g., C:\Users\...\HytaleF2P) + * @param {string} branch - Branch name (release or pre-release) + * @param {boolean} hasVersionConfig - True if config.json has version_client and version_branch + * @returns {Promise} - Path to backup or null if no UserData found + */ + async backupUserData(installPath, branch, hasVersionConfig = true) { + let userDataPath; + + // Si on n'a pas de version_client/version_branch dans config.json, + // c'est une ancienne installation, on cherche dans installPath/HytaleF2P/release + if (!hasVersionConfig) { + const oldPath = path.join(installPath, 'HytaleF2P', 'release', 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataBackup] Pas de version_client/version_branch détecté, recherche ancienne installation dans: ${oldPath}`); + + if (fs.existsSync(oldPath)) { + userDataPath = oldPath; + console.log(`[UserDataBackup] ✓ Ancienne installation trouvée ! UserData existe dans l'ancien emplacement`); + } else { + console.log(`[UserDataBackup] ✗ Aucune ancienne installation trouvée dans ${oldPath}`); + userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + } + } else { + // Si on a version_client/version_branch, on cherche dans installPath/HytaleF2P/ + userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataBackup] Version configurée, recherche dans: ${userDataPath}`); + } + + if (!fs.existsSync(userDataPath)) { + console.log(`[UserDataBackup] ✗ Aucun UserData trouvé à ${userDataPath}, backup ignoré`); + return null; + } + + console.log(`[UserDataBackup] ✓ UserData trouvé à ${userDataPath}`); + const backupPath = path.join(installPath, `UserData_backup_${branch}_${Date.now()}`); + + try { + console.log(`[UserDataBackup] Copie de ${userDataPath} vers ${backupPath}...`); + await fs.copy(userDataPath, backupPath, { + overwrite: true, + errorOnExist: false + }); + console.log('[UserDataBackup] ✓ Backup complété avec succès'); + return backupPath; + } catch (error) { + console.error('[UserDataBackup] ✗ Erreur lors du backup:', error); + throw new Error(`Failed to backup UserData: ${error.message}`); + } + } + + /** + * Restore UserData folder from backup + * @param {string} backupPath - Path to the backup folder + * @param {string} installPath - Base installation path + * @param {string} branch - Branch name (release or pre-release) + * @returns {Promise} - True if restored, false otherwise + */ + async restoreUserData(backupPath, installPath, branch) { + if (!backupPath || !fs.existsSync(backupPath)) { + console.log('No backup to restore or backup path does not exist'); + return false; + } + + const userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + + try { + console.log(`Restoring UserData from ${backupPath} to ${userDataPath}`); + + // Ensure parent directory exists + const parentDir = path.dirname(userDataPath); + if (!fs.existsSync(parentDir)) { + await fs.ensureDir(parentDir); + } + + await fs.copy(backupPath, userDataPath, { + overwrite: true, + errorOnExist: false + }); + + console.log('UserData restore completed successfully'); + return true; + } catch (error) { + console.error('Error restoring UserData:', error); + throw new Error(`Failed to restore UserData: ${error.message}`); + } + } + + /** + * Clean up backup folder + * @param {string} backupPath - Path to the backup folder to delete + * @returns {Promise} - True if deleted, false otherwise + */ + async cleanupBackup(backupPath) { + if (!backupPath || !fs.existsSync(backupPath)) { + return false; + } + + try { + console.log(`Cleaning up backup at ${backupPath}`); + await fs.remove(backupPath); + console.log('Backup cleanup completed'); + return true; + } catch (error) { + console.error('Error cleaning up backup:', error); + return false; + } + } + + /** + * Check if UserData exists for a specific branch + * @param {string} installPath - Base installation path + * @param {string} branch - Branch name (release or pre-release) + * @returns {boolean} - True if UserData exists + */ + hasUserData(installPath, branch) { + const userDataPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + return fs.existsSync(userDataPath); + } +} + +module.exports = new UserDataBackup(); diff --git a/main.js b/main.js index 9fe72d6..d47d94c 100644 --- a/main.js +++ b/main.js @@ -2,8 +2,8 @@ const path = require('path'); require('dotenv').config({ path: path.join(__dirname, '.env') }); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const fs = require('fs'); -const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); - +const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); + const UpdateManager = require('./backend/updateManager'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); @@ -187,21 +187,21 @@ function createWindow() { if (input.key === 'F12') { event.preventDefault(); } - if (input.key === 'F5') { - event.preventDefault(); - } - - // Close application shortcuts - const isMac = process.platform === 'darwin'; - const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') || - (!isMac && input.control && input.key.toLowerCase() === 'q') || - (!isMac && input.alt && input.key === 'F4'); - - if (quitShortcut) { - app.quit(); - } - }); - + if (input.key === 'F5') { + event.preventDefault(); + } + + // Close application shortcuts + const isMac = process.platform === 'darwin'; + const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') || + (!isMac && input.control && input.key.toLowerCase() === 'q') || + (!isMac && input.alt && input.key === 'F4'); + + if (quitShortcut) { + app.quit(); + } + }); + mainWindow.webContents.on('context-menu', (e) => { @@ -345,14 +345,14 @@ app.on('before-quit', () => { cleanupDiscordRPC(); }); -app.on('window-all-closed', () => { - console.log('=== LAUNCHER CLOSING ==='); - - cleanupDiscordRPC(); - - app.quit(); -}); - +app.on('window-all-closed', () => { + console.log('=== LAUNCHER CLOSING ==='); + + cleanupDiscordRPC(); + + app.quit(); +}); + ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => { try { @@ -369,20 +369,20 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g } }; - const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); - - if (result.success && result.launched) { - const closeOnStart = loadCloseLauncherOnStart(); - if (closeOnStart) { - console.log('Close Launcher on start enabled, quitting application...'); - setTimeout(() => { - app.quit(); - }, 1000); - } - } - - return result; - + const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); + + if (result.success && result.launched) { + const closeOnStart = loadCloseLauncherOnStart(); + if (closeOnStart) { + console.log('Close Launcher on start enabled, quitting application...'); + setTimeout(() => { + app.quit(); + }, 1000); + } + } + + return result; + } catch (error) { console.error('Launch error:', error); const errorMessage = error.message || error.toString(); @@ -397,8 +397,10 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g } }); -ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) => { +ipcMain.handle('install-game', async (event, playerName, javaPath, installPath, branch) => { try { + console.log(`[IPC] install-game called with branch: ${branch || 'default'}`); + // Signal installation start if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.webContents.send('installation-start'); @@ -417,7 +419,7 @@ ipcMain.handle('install-game', async (event, playerName, javaPath, installPath) } }; - const result = await installGame(playerName, progressCallback, javaPath, installPath); + const result = await installGame(playerName, progressCallback, javaPath, installPath, branch); // Signal installation end if (mainWindow && !mainWindow.isDestroyed()) { @@ -497,21 +499,21 @@ ipcMain.handle('save-language', (event, language) => { return { success: true }; }); -ipcMain.handle('load-language', () => { - return loadLanguage(); -}); - -ipcMain.handle('save-close-launcher', (event, enabled) => { - saveCloseLauncherOnStart(enabled); - return { success: true }; -}); - -ipcMain.handle('load-close-launcher', () => { - return loadCloseLauncherOnStart(); -}); - -ipcMain.handle('select-install-path', async () => { - +ipcMain.handle('load-language', () => { + return loadLanguage(); +}); + +ipcMain.handle('save-close-launcher', (event, enabled) => { + saveCloseLauncherOnStart(enabled); + return { success: true }; +}); + +ipcMain.handle('load-close-launcher', () => { + return loadCloseLauncherOnStart(); +}); + +ipcMain.handle('select-install-path', async () => { + const result = await dialog.showOpenDialog(mainWindow, { properties: ['openDirectory'], title: 'Select Installation Folder' @@ -836,10 +838,10 @@ ipcMain.handle('open-download-page', async () => { try { await shell.openExternal(updateManager.getDownloadUrl()); - setTimeout(() => { - app.quit(); - }, 1000); - + setTimeout(() => { + app.quit(); + }, 1000); + return { success: true }; } catch (error) { @@ -881,10 +883,26 @@ ipcMain.handle('get-detected-gpu', () => { return global.detectedGpu; }); -ipcMain.handle('window-close', () => { - app.quit(); -}); - +ipcMain.handle('save-version-branch', (event, branch) => { + const { saveVersionBranch } = require('./backend/launcher'); + saveVersionBranch(branch); + return { success: true }; +}); + +ipcMain.handle('load-version-branch', () => { + const { loadVersionBranch } = require('./backend/launcher'); + return loadVersionBranch(); +}); + +ipcMain.handle('load-version-client', () => { + const { loadVersionClient } = require('./backend/launcher'); + return loadVersionClient(); +}); + +ipcMain.handle('window-close', () => { + app.quit(); +}); + ipcMain.handle('window-minimize', () => { if (mainWindow && !mainWindow.isDestroyed()) { diff --git a/package.json b/package.json index 26e43a6..0514d82 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "discord-rpc": "^4.0.1", "dotenv": "^17.2.3", "electron-updater": "^6.7.3", + "fs-extra": "^11.3.3", "tar": "^6.2.1", "uuid": "^9.0.1" }, diff --git a/preload.js b/preload.js index b00d840..b431f1a 100644 --- a/preload.js +++ b/preload.js @@ -68,6 +68,10 @@ contextBridge.exposeInMainWorld('electronAPI', { loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'), getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'), + saveVersionBranch: (branch) => ipcRenderer.invoke('save-version-branch', branch), + loadVersionBranch: () => ipcRenderer.invoke('load-version-branch'), + loadVersionClient: () => ipcRenderer.invoke('load-version-client'), + acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame), markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'), onFirstLaunchUpdate: (callback) => {