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" />
+
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) => {