pre-release & release game version [to check]

This commit is contained in:
AMIAY
2026-01-23 17:54:57 +01:00
parent 08c2218cf8
commit 3983fdb1bc
19 changed files with 888 additions and 302 deletions

View File

@@ -123,6 +123,26 @@
value="Player" /> value="Player" />
</div> </div>
<div class="form-group">
<label class="form-label" data-i18n="install.gameBranch">Game Version</label>
<div class="radio-group">
<label class="radio-label">
<input type="radio" name="installBranch" value="release" class="custom-radio" checked>
<span class="radio-text">
<i class="fas fa-check-circle mr-2"></i>
<span data-i18n="install.releaseVersion">Release (Stable)</span>
</span>
</label>
<label class="radio-label">
<input type="radio" name="installBranch" value="pre-release" class="custom-radio">
<span class="radio-text">
<i class="fas fa-flask mr-2"></i>
<span data-i18n="install.preReleaseVersion">Pre-Release (Experimental)</span>
</span>
</label>
</div>
</div>
<div class="form-group"> <div class="form-group">
<label class="checkbox-group"> <label class="checkbox-group">
<input type="checkbox" id="installCustomCheck" class="custom-checkbox"> <input type="checkbox" id="installCustomCheck" class="custom-checkbox">
@@ -337,9 +357,29 @@
</div> </div>
</button> </button>
</div> </div>
</div>
<div class="settings-option">
<div class="settings-input-group"> <div class="settings-input-group">
<label class="settings-input-label" data-i18n="settings.gameBranch">Game Branch</label>
<div class="segmented-control">
<input type="radio" id="branch-release" name="gameBranch" value="release" checked>
<label for="branch-release" data-i18n="settings.branchRelease">Release</label>
<input type="radio" id="branch-pre-release" name="gameBranch" value="pre-release">
<label for="branch-pre-release" data-i18n="settings.branchPreRelease">Pre-Release</label>
</div>
<p class="settings-hint">
<i class="fas fa-info-circle"></i>
<span data-i18n="settings.branchHint">Switch between stable release and experimental pre-release versions</span>
</p>
<p class="settings-hint" style="color: #f39c12;">
<i class="fas fa-exclamation-triangle"></i>
<span data-i18n="settings.branchWarning">Changing branch will download and install a different game version</span>
</p>
</div>
</div>
<div class="settings-option">
<label class="settings-input-label" data-i18n="settings.gpuPreference">GPU <label class="settings-input-label" data-i18n="settings.gpuPreference">GPU
Preference</label> Preference</label>
<div class="segmented-control"> <div class="segmented-control">

View File

@@ -60,6 +60,18 @@ export async function installGame() {
const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player'; const playerName = (installPlayerName ? installPlayerName.value.trim() : '') || 'Player';
const installPath = installPathInput ? installPathInput.value.trim() : ''; 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(); if (window.LauncherUI) window.LauncherUI.showProgress();
isDownloading = true; isDownloading = true;
if (installBtn) { if (installBtn) {
@@ -69,7 +81,7 @@ export async function installGame() {
try { try {
if (window.electronAPI && window.electronAPI.installGame) { 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) { if (result.success) {
const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!'; const successMsg = window.i18n ? window.i18n.t('progress.installationComplete') : 'Installation completed successfully!';

View File

@@ -3,11 +3,12 @@ let customJavaCheck;
let customJavaOptions; let customJavaOptions;
let customJavaPath; let customJavaPath;
let browseJavaBtn; let browseJavaBtn;
let settingsPlayerName; let settingsPlayerName;
let discordRPCCheck; let discordRPCCheck;
let closeLauncherCheck; let closeLauncherCheck;
let gpuPreferenceRadios; let gpuPreferenceRadios;
let gameBranchRadios;
// UUID Management elements // UUID Management elements
let currentUuidDisplay; let currentUuidDisplay;
@@ -161,11 +162,12 @@ function setupSettingsElements() {
customJavaOptions = document.getElementById('customJavaOptions'); customJavaOptions = document.getElementById('customJavaOptions');
customJavaPath = document.getElementById('customJavaPath'); customJavaPath = document.getElementById('customJavaPath');
browseJavaBtn = document.getElementById('browseJavaBtn'); browseJavaBtn = document.getElementById('browseJavaBtn');
settingsPlayerName = document.getElementById('settingsPlayerName'); settingsPlayerName = document.getElementById('settingsPlayerName');
discordRPCCheck = document.getElementById('discordRPCCheck'); discordRPCCheck = document.getElementById('discordRPCCheck');
closeLauncherCheck = document.getElementById('closeLauncherCheck'); closeLauncherCheck = document.getElementById('closeLauncherCheck');
gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]'); gpuPreferenceRadios = document.querySelectorAll('input[name="gpuPreference"]');
gameBranchRadios = document.querySelectorAll('input[name="gameBranch"]');
// UUID Management elements // UUID Management elements
currentUuidDisplay = document.getElementById('currentUuid'); currentUuidDisplay = document.getElementById('currentUuid');
@@ -194,14 +196,14 @@ function setupSettingsElements() {
settingsPlayerName.addEventListener('change', savePlayerName); settingsPlayerName.addEventListener('change', savePlayerName);
} }
if (discordRPCCheck) { if (discordRPCCheck) {
discordRPCCheck.addEventListener('change', saveDiscordRPC); discordRPCCheck.addEventListener('change', saveDiscordRPC);
} }
if (closeLauncherCheck) { if (closeLauncherCheck) {
closeLauncherCheck.addEventListener('change', saveCloseLauncher); closeLauncherCheck.addEventListener('change', saveCloseLauncher);
} }
// UUID event listeners // UUID event listeners
if (copyUuidBtn) { if (copyUuidBtn) {
@@ -252,6 +254,12 @@ function setupSettingsElements() {
}); });
}); });
} }
if (gameBranchRadios) {
gameBranchRadios.forEach(radio => {
radio.addEventListener('change', handleBranchChange);
});
}
} }
function toggleCustomJava() { function toggleCustomJava() {
@@ -344,43 +352,43 @@ async function saveDiscordRPC() {
} }
} }
async function loadDiscordRPC() { async function loadDiscordRPC() {
try { try {
if (window.electronAPI && window.electronAPI.loadDiscordRPC) { if (window.electronAPI && window.electronAPI.loadDiscordRPC) {
const enabled = await window.electronAPI.loadDiscordRPC(); const enabled = await window.electronAPI.loadDiscordRPC();
if (discordRPCCheck) { if (discordRPCCheck) {
discordRPCCheck.checked = enabled; discordRPCCheck.checked = enabled;
} }
} }
} catch (error) { } catch (error) {
console.error('Error loading Discord RPC setting:', error); console.error('Error loading Discord RPC setting:', error);
} }
} }
async function saveCloseLauncher() { async function saveCloseLauncher() {
try { try {
if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) { if (window.electronAPI && window.electronAPI.saveCloseLauncher && closeLauncherCheck) {
const enabled = closeLauncherCheck.checked; const enabled = closeLauncherCheck.checked;
await window.electronAPI.saveCloseLauncher(enabled); await window.electronAPI.saveCloseLauncher(enabled);
} }
} catch (error) { } catch (error) {
console.error('Error saving close launcher setting:', error); console.error('Error saving close launcher setting:', error);
} }
} }
async function loadCloseLauncher() { async function loadCloseLauncher() {
try { try {
if (window.electronAPI && window.electronAPI.loadCloseLauncher) { if (window.electronAPI && window.electronAPI.loadCloseLauncher) {
const enabled = await window.electronAPI.loadCloseLauncher(); const enabled = await window.electronAPI.loadCloseLauncher();
if (closeLauncherCheck) { if (closeLauncherCheck) {
closeLauncherCheck.checked = enabled; closeLauncherCheck.checked = enabled;
} }
} }
} catch (error) { } catch (error) {
console.error('Error loading close launcher setting:', error); console.error('Error loading close launcher setting:', error);
} }
} }
async function savePlayerName() { async function savePlayerName() {
try { try {
@@ -491,15 +499,16 @@ async function loadGpuPreference() {
} }
} }
async function loadAllSettings() { async function loadAllSettings() {
await loadCustomJavaPath(); await loadCustomJavaPath();
await loadPlayerName(); await loadPlayerName();
await loadCurrentUuid(); await loadCurrentUuid();
await loadDiscordRPC(); await loadDiscordRPC();
await loadCloseLauncher(); await loadCloseLauncher();
await loadGpuPreference(); await loadGpuPreference();
} await loadVersionBranch();
}
async function openGameLocation() { async function openGameLocation() {
try { try {
@@ -891,4 +900,177 @@ function showNotification(message, type = 'info') {
} }
}, 300); }, 300);
}, 3000); }, 3000);
} }// 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';
}
}

View File

@@ -501,6 +501,7 @@ function setupUI() {
setupAnimations(); setupAnimations();
setupFirstLaunchHandlers(); setupFirstLaunchHandlers();
loadLauncherVersion(); loadLauncherVersion();
checkGameInstallation();
document.body.focus(); 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 = { window.LauncherUI = {
showPage, showPage,
setActiveNav, setActiveNav,

View File

@@ -15,6 +15,9 @@
"title": "FREE TO PLAY LAUNCHER", "title": "FREE TO PLAY LAUNCHER",
"playerName": "Player Name", "playerName": "Player Name",
"playerNamePlaceholder": "Enter your name", "playerNamePlaceholder": "Enter your name",
"gameBranch": "Game Version",
"releaseVersion": "Release (Stable)",
"preReleaseVersion": "Pre-Release (Experimental)",
"customInstallation": "Custom Installation", "customInstallation": "Custom Installation",
"installationFolder": "Installation Folder", "installationFolder": "Installation Folder",
"pathPlaceholder": "Default location", "pathPlaceholder": "Default location",
@@ -125,7 +128,16 @@
"logsLoading": "Loading logs...", "logsLoading": "Loading logs...",
"closeLauncher": "Launcher Behavior", "closeLauncher": "Launcher Behavior",
"closeOnStart": "Close Launcher on game start", "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": { "uuid": {
"modalTitle": "UUID Management", "modalTitle": "UUID Management",
@@ -157,7 +169,8 @@
"delete": "Delete", "delete": "Delete",
"edit": "Edit", "edit": "Edit",
"loading": "Loading...", "loading": "Loading...",
"apply": "Apply" "apply": "Apply",
"install": "Install"
}, },
"notifications": { "notifications": {
"gameDataNotFound": "Error: Game data not found", "gameDataNotFound": "Error: Game data not found",

View File

@@ -14,8 +14,9 @@
"install": { "install": {
"title": "LAUNCHER GRATUITO", "title": "LAUNCHER GRATUITO",
"playerName": "Nombre del Jugador", "playerName": "Nombre del Jugador",
"playerNamePlaceholder": "Ingresa tu nombre", "playerNamePlaceholder": "Ingresa tu nombre", "gameBranch": "Versión del Juego",
"customInstallation": "Instalación Personalizada", "releaseVersion": "Lanzamiento (Estable)",
"preReleaseVersion": "Pre-Lanzamiento (Experimental)", "customInstallation": "Instalación Personalizada",
"installationFolder": "Carpeta de Instalación", "installationFolder": "Carpeta de Instalación",
"pathPlaceholder": "Ubicación predeterminada", "pathPlaceholder": "Ubicación predeterminada",
"browse": "Examinar", "browse": "Examinar",
@@ -125,7 +126,16 @@
"logsLoading": "Cargando registros...", "logsLoading": "Cargando registros...",
"closeLauncher": "Comportamiento del Launcher", "closeLauncher": "Comportamiento del Launcher",
"closeOnStart": "Cerrar Launcher al iniciar el juego", "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": { "uuid": {
"modalTitle": "Gestión de UUID", "modalTitle": "Gestión de UUID",
@@ -157,7 +167,8 @@
"delete": "Eliminar", "delete": "Eliminar",
"edit": "Editar", "edit": "Editar",
"loading": "Cargando...", "loading": "Cargando...",
"apply": "Aplicar" "apply": "Aplicar",
"install": "Instalar"
}, },
"notifications": { "notifications": {
"gameDataNotFound": "Error: No se encontraron datos del juego", "gameDataNotFound": "Error: No se encontraron datos del juego",

View File

@@ -14,8 +14,9 @@
"install": { "install": {
"title": "LANÇADOR JOGO GRATUITO", "title": "LANÇADOR JOGO GRATUITO",
"playerName": "Nome do Jogador", "playerName": "Nome do Jogador",
"playerNamePlaceholder": "Digite seu nome", "playerNamePlaceholder": "Digite seu nome", "gameBranch": "Versão do Jogo",
"customInstallation": "Instalação Personalizada", "releaseVersion": "Lançamento (Estável)",
"preReleaseVersion": "Pré-Lançamento (Experimental)", "customInstallation": "Instalação Personalizada",
"installationFolder": "Pasta de Instalação", "installationFolder": "Pasta de Instalação",
"pathPlaceholder": "Local padrão", "pathPlaceholder": "Local padrão",
"browse": "Procurar", "browse": "Procurar",
@@ -125,7 +126,16 @@
"logsLoading": "Carregando registros...", "logsLoading": "Carregando registros...",
"closeLauncher": "Comportamento do Lançador", "closeLauncher": "Comportamento do Lançador",
"closeOnStart": "Fechar Lançador ao iniciar o jogo", "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": { "uuid": {
"modalTitle": "Gerenciamento de UUID", "modalTitle": "Gerenciamento de UUID",
@@ -158,7 +168,8 @@
"delete": "Excluir", "delete": "Excluir",
"edit": "Editar", "edit": "Editar",
"loading": "Carregando...", "loading": "Carregando...",
"apply": "Aplicar" "apply": "Aplicar",
"install": "Instalar"
}, },
"notifications": { "notifications": {
"gameDataNotFound": "Erro: Dados do jogo não encontrados", "gameDataNotFound": "Erro: Dados do jogo não encontrados",

View File

@@ -662,6 +662,57 @@ body {
box-shadow: none; 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 { .launcher-container {
flex: 1; flex: 1;
display: flex; display: flex;

View File

@@ -304,6 +304,30 @@ function loadGpuPreference() {
return config.gpuPreference || 'auto'; 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 = { module.exports = {
loadConfig, loadConfig,
saveConfig, saveConfig,
@@ -343,5 +367,10 @@ module.exports = {
loadGpuPreference, loadGpuPreference,
// Close Launcher export // Close Launcher export
saveCloseLauncherOnStart, saveCloseLauncherOnStart,
loadCloseLauncherOnStart loadCloseLauncherOnStart,
// Version Management exports
saveVersionClient,
loadVersionClient,
saveVersionBranch,
loadVersionBranch
}; };

View File

@@ -33,7 +33,12 @@ const {
resetCurrentUserUuid, resetCurrentUserUuid,
// GPU Preference // GPU Preference
saveGpuPreference, saveGpuPreference,
loadGpuPreference loadGpuPreference,
// Version Management
saveVersionClient,
loadVersionClient,
saveVersionBranch,
loadVersionBranch
} = require('./core/config'); } = require('./core/config');
const { getResolvedAppDir, getModsPath } = require('./core/paths'); const { getResolvedAppDir, getModsPath } = require('./core/paths');
@@ -138,6 +143,10 @@ module.exports = {
// Version functions // Version functions
getInstalledClientVersion, getInstalledClientVersion,
getLatestClientVersion, getLatestClientVersion,
saveVersionClient,
loadVersionClient,
saveVersionBranch,
loadVersionBranch,
// News functions // News functions
getHytaleNews, getHytaleNews,

View File

@@ -7,7 +7,7 @@ const { spawn } = require('child_process');
const { v4: uuidv4 } = require('uuid'); const { v4: uuidv4 } = require('uuid');
const { getResolvedAppDir, findClientPath } = require('../core/paths'); const { getResolvedAppDir, findClientPath } = require('../core/paths');
const { setupWaylandEnvironment, setupGpuEnvironment } = require('../utils/platformUtils'); 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 { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA_EXECUTABLE } = require('./javaManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('../services/versionManager');
const { updateGameFiles } = require('./gameManager'); 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 customAppDir = getResolvedAppDir(installPathOverride);
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest'); const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData'); const userDataDir = path.join(customGameDir, 'Client', 'UserData');
const gameLatest = customGameDir; const gameLatest = customGameDir;
@@ -151,13 +152,14 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName); const { identityToken, sessionToken } = await fetchAuthTokens(uuid, playerName);
// Patch client and server binaries to use custom auth server (BEFORE signing on macOS) // 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(); const authDomain = getAuthDomain();
if (clientPatcher) { if (clientPatcher) {
try { try {
if (progressCallback) { if (progressCallback) {
progressCallback('Patching game for custom server...', null, null, null, null); 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) => { const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
console.log(`[Patcher] ${msg}`); console.log(`[Patcher] ${msg}`);
@@ -167,16 +169,12 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
}); });
if (patchResult.success) { if (patchResult.success) {
if (patchResult.alreadyPatched) { console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`);
console.log(`Game already patched for ${authDomain}`); if (patchResult.client) {
} else { console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`);
console.log(`Game patched successfully (${patchResult.patchCount} total occurrences)`); }
if (patchResult.client) { if (patchResult.server) {
console.log(` Client: ${patchResult.client.patchCount || 0} occurrences`); console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
}
if (patchResult.server) {
console.log(` Server: ${patchResult.server.patchCount || 0} occurrences`);
}
} }
} else { } else {
console.warn('Game patching failed:', patchResult.error); 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 { try {
const branch = branchOverride || loadVersionBranch();
if (progressCallback) { if (progressCallback) {
progressCallback('Checking for updates...', 0, null, null, null); progressCallback('Checking for updates...', 0, null, null, null);
} }
const [installedVersion, latestVersion] = await Promise.all([ const installedVersion = loadVersionClient();
getInstalledClientVersion(), const latestVersion = await getLatestClientVersion(branch);
getLatestClientVersion()
]);
console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion}`); console.log(`Installed version: ${installedVersion}, Latest version: ${latestVersion} (branch: ${branch})`);
let needsUpdate = false; let needsUpdate = false;
if (installedVersion && latestVersion && installedVersion !== latestVersion) { if (!installedVersion || installedVersion !== latestVersion) {
needsUpdate = true; needsUpdate = true;
console.log('Version mismatch detected, update required'); console.log('Version mismatch or not installed, update required');
} }
if (needsUpdate) { if (needsUpdate) {
@@ -380,13 +378,13 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
} }
const customAppDir = getResolvedAppDir(installPathOverride); 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 customToolsDir = path.join(customAppDir, 'butler');
const customCacheDir = path.join(customAppDir, 'cache'); const customCacheDir = path.join(customAppDir, 'cache');
try { try {
await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir); await updateGameFiles(latestVersion, progressCallback, customGameDir, customToolsDir, customCacheDir, branch);
console.log('Game updated successfully, waiting before launch...'); console.log('Game updated successfully, patching will be forced on launch...');
if (progressCallback) { if (progressCallback) {
progressCallback('Preparing game launch...', 90, null, null, null); 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); 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) { } catch (error) {
console.error('Error in version check and launch:', error); console.error('Error in version check and launch:', error);
if (progressCallback) { if (progressCallback) {

View File

@@ -7,10 +7,11 @@ const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager'); const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
const { installButler } = require('./butlerManager'); const { installButler } = require('./butlerManager');
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); 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 { 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 osName = getOS();
const arch = getArch(); 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.'); 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)) { if (fs.existsSync(dest)) {
console.log('PWR file found in cache:', 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'); 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 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 { try {
if (progressCallback) { if (progressCallback) {
progressCallback('Updating game files...', 0, null, null, null); 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'); 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); 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) { if (progressCallback) {
progressCallback('Extracting new files...', 50, null, null, null); 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); await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
if (progressCallback) { if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null); progressCallback('Backing up user data...', 70, null, null, null);
} }
let userDataBackup = null; // Backup UserData using new system
const userDataPath = findUserDataRecursive(gameDir); try {
backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig);
} catch (backupError) {
console.warn('UserData backup failed:', backupError.message);
}
if (userDataPath && fs.existsSync(userDataPath)) { if (progressCallback) {
userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now()); progressCallback('Replacing game files...', 80, null, null, null);
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 (fs.existsSync(gameDir)) { if (fs.existsSync(gameDir)) {
@@ -175,44 +169,26 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback); const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback);
console.log('Logo@2x.png update result after update:', logoResult); console.log('Logo@2x.png update result after update:', logoResult);
if (userDataBackup && fs.existsSync(userDataBackup)) { if (progressCallback) {
const newUserDataPath = findUserDataPath(gameDir); progressCallback('Restoring user data...', 90, null, null, null);
const userDataParent = path.dirname(newUserDataPath); }
if (!fs.existsSync(userDataParent)) { // Restore UserData using new system
fs.mkdirSync(userDataParent, { recursive: true }); 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}`); console.log(`Game files updated successfully to version: ${newVersion}`);
if (userDataBackup && fs.existsSync(userDataBackup)) { // Save the updated version and branch to config
try { saveVersionClient(newVersion);
fs.rmSync(userDataBackup, { recursive: true, force: true }); const { saveVersionBranch } = require('../core/config');
console.log('UserData backup cleaned up'); saveVersionBranch(branch);
} catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message);
}
}
console.log('Waiting for file system sync...'); console.log('Waiting for file system sync...');
await new Promise(resolve => setTimeout(resolve, 2000)); await new Promise(resolve => setTimeout(resolve, 2000));
@@ -225,9 +201,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
} catch (error) { } catch (error) {
console.error('Error updating game files:', error); console.error('Error updating game files:', error);
if (userDataBackup && fs.existsSync(userDataBackup)) { if (backupPath) {
try { try {
fs.rmSync(userDataBackup, { recursive: true, force: true }); await userDataBackup.cleanupBackup(backupPath);
console.log('UserData backup cleaned up after error'); console.log('UserData backup cleaned up after error');
} catch (cleanupError) { } catch (cleanupError) {
console.warn('Could not clean up UserData backup:', cleanupError.message); 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 appDir = getResolvedAppDir();
const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); const gameDir = path.join(appDir, branch, 'package', 'game', 'latest');
const clientPath = findClientPath(gameDir); const clientPath = findClientPath(gameDir);
return clientPath !== null; 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 customAppDir = getResolvedAppDir(installPathOverride);
const customCacheDir = path.join(customAppDir, 'cache'); const customCacheDir = path.join(customAppDir, 'cache');
const customToolsDir = path.join(customAppDir, 'butler'); const customToolsDir = path.join(customAppDir, 'butler');
const customGameDir = path.join(customAppDir, 'release', 'package', 'game', 'latest'); const customGameDir = path.join(customAppDir, branch, 'package', 'game', 'latest');
const customJreDir = path.join(customAppDir, 'release', 'package', 'jre', 'latest'); const customJreDir = path.join(customAppDir, branch, 'package', 'jre', 'latest');
const userDataDir = path.join(customGameDir, 'Client', 'UserData'); 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 => { [customAppDir, customCacheDir, customToolsDir].forEach(dir => {
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true }); fs.mkdirSync(dir, { recursive: true });
@@ -313,18 +313,47 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver
if (progressCallback) { if (progressCallback) {
progressCallback('Fetching game files...', null, null, null, null); 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 latestVersion = await getLatestClientVersion(branch);
const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); const pwrFile = await downloadPWR(branch, latestVersion, progressCallback, customCacheDir);
await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); 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); const homeUIResult = await downloadAndReplaceHomePageUI(customGameDir, progressCallback);
console.log('HomePage.ui update result after installation:', homeUIResult); console.log('HomePage.ui update result after installation:', homeUIResult);
const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback); const logoResult = await downloadAndReplaceLogo(customGameDir, progressCallback);
console.log('Logo@2x.png update result after installation:', logoResult); 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) { if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null); progressCallback('Installation complete', 100, null, null, null);
} }
@@ -357,8 +386,9 @@ async function uninstallGame() {
} }
} }
function checkExistingGameInstallation() { function checkExistingGameInstallation(branchOverride = null) {
try { try {
const branch = branchOverride || loadVersionBranch();
const config = loadConfig(); const config = loadConfig();
if (!config.installPath || !config.installPath.trim()) { if (!config.installPath || !config.installPath.trim()) {
@@ -366,7 +396,7 @@ function checkExistingGameInstallation() {
} }
const installPath = config.installPath.trim(); 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)) { if (!fs.existsSync(gameDir)) {
return null; return null;
@@ -384,7 +414,8 @@ function checkExistingGameInstallation() {
clientPath: clientPath, clientPath: clientPath,
userDataPath: userDataPath, userDataPath: userDataPath,
installPath: installPath, installPath: installPath,
hasUserData: userDataPath && fs.existsSync(userDataPath) hasUserData: userDataPath && fs.existsSync(userDataPath),
branch: branch
}; };
} catch (error) { } catch (error) {
console.error('Error checking existing game installation:', 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 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 // Check if game exists
if (!fs.existsSync(gameDir)) { if (!fs.existsSync(gameDir)) {
throw new Error('Game directory not found. Cannot repair.'); throw new Error('Game directory not found. Cannot repair.');
} }
// Locate UserData
const userDataPath = findUserDataRecursive(gameDir);
let userDataBackup = null;
if (progressCallback) { if (progressCallback) {
progressCallback('Backing up user data...', 10, null, null, null); progressCallback('Backing up user data...', 10, null, null, null);
} }
// Backup UserData // Backup UserData using new system
if (userDataPath && fs.existsSync(userDataPath)) { try {
userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now()); backupPath = await userDataBackup.backupUserData(installPath, branch, hasVersionConfig);
console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`); } catch (backupError) {
console.warn('UserData backup failed during repair:', backupError.message);
// 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);
} }
if (progressCallback) { if (progressCallback) {
@@ -446,39 +469,21 @@ async function repairGame(progressCallback) {
// Passing null/undefined for overrides to use defaults/saved configs // Passing null/undefined for overrides to use defaults/saved configs
// installGame calls progressCallback internally // installGame calls progressCallback internally
await installGame('Player', progressCallback); await installGame('Player', progressCallback, null, null, branch);
// Restore UserData // Restore UserData using new system
if (userDataBackup && fs.existsSync(userDataBackup)) { if (backupPath) {
if (progressCallback) { if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null); progressCallback('Restoring user data...', 90, null, null, null);
} }
// installGame creates: path.join(customGameDir, 'Client', 'UserData') try {
const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); await userDataBackup.restoreUserData(backupPath, installPath, branch);
const newUserDataPath = path.join(newGameDir, 'Client', 'UserData'); await userDataBackup.cleanupBackup(backupPath);
console.log('UserData restored successfully after repair');
if (!fs.existsSync(newUserDataPath)) { } catch (restoreError) {
fs.mkdirSync(newUserDataPath, { recursive: true }); 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) { if (progressCallback) {

View File

@@ -1,6 +1,6 @@
const path = require('path'); const path = require('path');
const fs = require('fs'); 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 { checkExistingGameInstallation, updateGameFiles } = require('../managers/gameManager');
const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager'); const { getInstalledClientVersion, getLatestClientVersion } = require('./versionManager');
@@ -56,6 +56,20 @@ async function handleFirstLaunchCheck(progressCallback) {
try { try {
const config = loadConfig(); 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) { if (config.hasLaunchedBefore === true) {
return { isFirstLaunch: false, needsUpdate: false }; return { isFirstLaunch: false, needsUpdate: false };
} }

View File

@@ -1,9 +1,10 @@
const axios = require('axios'); const axios = require('axios');
async function getLatestClientVersion() { async function getLatestClientVersion(branch = 'release') {
try { 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', { const response = await axios.get('https://files.hytalef2p.com/api/version_client', {
params: { branch },
timeout: 5000, timeout: 5000,
headers: { headers: {
'User-Agent': 'Hytale-F2P-Launcher' 'User-Agent': 'Hytale-F2P-Launcher'
@@ -12,7 +13,7 @@ async function getLatestClientVersion() {
if (response.data && response.data.client_version) { if (response.data && response.data.client_version) {
const version = 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; return version;
} else { } else {
console.log('Warning: Invalid API response, falling back to default version'); console.log('Warning: Invalid API response, falling back to default version');

View File

@@ -11,6 +11,18 @@ class UpdateManager {
} }
async checkForUpdates() { 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 { try {
console.log('Checking for updates...'); console.log('Checking for updates...');
console.log(`Local version: ${CURRENT_VERSION}`); console.log(`Local version: ${CURRENT_VERSION}`);
@@ -54,6 +66,7 @@ class UpdateManager {
currentVersion: CURRENT_VERSION currentVersion: CURRENT_VERSION
}; };
} }
*/
} }
getDownloadUrl() { getDownloadUrl() {

View File

@@ -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<string|null>} - 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/<branch>
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<boolean>} - 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<boolean>} - 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();

146
main.js
View File

@@ -2,8 +2,8 @@ const path = require('path');
require('dotenv').config({ path: path.join(__dirname, '.env') }); require('dotenv').config({ path: path.join(__dirname, '.env') });
const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron');
const fs = require('fs'); 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 UpdateManager = require('./backend/updateManager');
const logger = require('./backend/logger'); const logger = require('./backend/logger');
const profileManager = require('./backend/managers/profileManager'); const profileManager = require('./backend/managers/profileManager');
@@ -187,21 +187,21 @@ function createWindow() {
if (input.key === 'F12') { if (input.key === 'F12') {
event.preventDefault(); event.preventDefault();
} }
if (input.key === 'F5') { if (input.key === 'F5') {
event.preventDefault(); event.preventDefault();
} }
// Close application shortcuts // Close application shortcuts
const isMac = process.platform === 'darwin'; const isMac = process.platform === 'darwin';
const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') || const quitShortcut = (isMac && input.meta && input.key.toLowerCase() === 'q') ||
(!isMac && input.control && input.key.toLowerCase() === 'q') || (!isMac && input.control && input.key.toLowerCase() === 'q') ||
(!isMac && input.alt && input.key === 'F4'); (!isMac && input.alt && input.key === 'F4');
if (quitShortcut) { if (quitShortcut) {
app.quit(); app.quit();
} }
}); });
mainWindow.webContents.on('context-menu', (e) => { mainWindow.webContents.on('context-menu', (e) => {
@@ -345,14 +345,14 @@ app.on('before-quit', () => {
cleanupDiscordRPC(); cleanupDiscordRPC();
}); });
app.on('window-all-closed', () => { app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ==='); console.log('=== LAUNCHER CLOSING ===');
cleanupDiscordRPC(); cleanupDiscordRPC();
app.quit(); app.quit();
}); });
ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => { ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, gpuPreference) => {
try { try {
@@ -369,20 +369,20 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g
} }
}; };
const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference);
if (result.success && result.launched) { if (result.success && result.launched) {
const closeOnStart = loadCloseLauncherOnStart(); const closeOnStart = loadCloseLauncherOnStart();
if (closeOnStart) { if (closeOnStart) {
console.log('Close Launcher on start enabled, quitting application...'); console.log('Close Launcher on start enabled, quitting application...');
setTimeout(() => { setTimeout(() => {
app.quit(); app.quit();
}, 1000); }, 1000);
} }
} }
return result; return result;
} catch (error) { } catch (error) {
console.error('Launch error:', error); console.error('Launch error:', error);
const errorMessage = error.message || error.toString(); 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 { try {
console.log(`[IPC] install-game called with branch: ${branch || 'default'}`);
// Signal installation start // Signal installation start
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
mainWindow.webContents.send('installation-start'); 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 // Signal installation end
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {
@@ -497,21 +499,21 @@ ipcMain.handle('save-language', (event, language) => {
return { success: true }; return { success: true };
}); });
ipcMain.handle('load-language', () => { ipcMain.handle('load-language', () => {
return loadLanguage(); return loadLanguage();
}); });
ipcMain.handle('save-close-launcher', (event, enabled) => { ipcMain.handle('save-close-launcher', (event, enabled) => {
saveCloseLauncherOnStart(enabled); saveCloseLauncherOnStart(enabled);
return { success: true }; return { success: true };
}); });
ipcMain.handle('load-close-launcher', () => { ipcMain.handle('load-close-launcher', () => {
return loadCloseLauncherOnStart(); return loadCloseLauncherOnStart();
}); });
ipcMain.handle('select-install-path', async () => { ipcMain.handle('select-install-path', async () => {
const result = await dialog.showOpenDialog(mainWindow, { const result = await dialog.showOpenDialog(mainWindow, {
properties: ['openDirectory'], properties: ['openDirectory'],
title: 'Select Installation Folder' title: 'Select Installation Folder'
@@ -836,10 +838,10 @@ ipcMain.handle('open-download-page', async () => {
try { try {
await shell.openExternal(updateManager.getDownloadUrl()); await shell.openExternal(updateManager.getDownloadUrl());
setTimeout(() => { setTimeout(() => {
app.quit(); app.quit();
}, 1000); }, 1000);
return { success: true }; return { success: true };
} catch (error) { } catch (error) {
@@ -881,10 +883,26 @@ ipcMain.handle('get-detected-gpu', () => {
return global.detectedGpu; return global.detectedGpu;
}); });
ipcMain.handle('window-close', () => { ipcMain.handle('save-version-branch', (event, branch) => {
app.quit(); 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', () => { ipcMain.handle('window-minimize', () => {
if (mainWindow && !mainWindow.isDestroyed()) { if (mainWindow && !mainWindow.isDestroyed()) {

View File

@@ -50,6 +50,7 @@
"discord-rpc": "^4.0.1", "discord-rpc": "^4.0.1",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"electron-updater": "^6.7.3", "electron-updater": "^6.7.3",
"fs-extra": "^11.3.3",
"tar": "^6.2.1", "tar": "^6.2.1",
"uuid": "^9.0.1" "uuid": "^9.0.1"
}, },

View File

@@ -68,6 +68,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'), loadGpuPreference: () => ipcRenderer.invoke('load-gpu-preference'),
getDetectedGpu: () => ipcRenderer.invoke('get-detected-gpu'), 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), acceptFirstLaunchUpdate: (existingGame) => ipcRenderer.invoke('accept-first-launch-update', existingGame),
markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'), markAsLaunched: () => ipcRenderer.invoke('mark-as-launched'),
onFirstLaunchUpdate: (callback) => { onFirstLaunchUpdate: (callback) => {