diff --git a/backend/core/paths.js b/backend/core/paths.js index 78a5289..ff82366 100644 --- a/backend/core/paths.js +++ b/backend/core/paths.js @@ -14,6 +14,21 @@ function getAppDir() { } } +/** + * Get centralized UserData saves directory (NEW in 2.1.2) + * UserData is now stored separately from game installation + */ +function getHytaleSavesDir() { + const home = os.homedir(); + if (process.platform === 'win32') { + return path.join(home, 'AppData', 'Local', 'HytaleSaves'); + } else if (process.platform === 'darwin') { + return path.join(home, 'Library', 'Application Support', 'HytaleSaves'); + } else { + return path.join(home, '.hytalesaves'); + } +} + const DEFAULT_APP_DIR = getAppDir(); function getResolvedAppDir(customPath) { @@ -218,20 +233,8 @@ async function getModsPath(customInstallPath = null) { function getProfilesDir(customInstallPath = null) { try { - // get UserData path - let installPath = customInstallPath; - if (!installPath) { - const configFile = path.join(DEFAULT_APP_DIR, 'config.json'); - if (fs.existsSync(configFile)) { - const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); - installPath = config.installPath || ''; - } - } - if (!installPath) installPath = getAppDir(); - - const branch = loadVersionBranch(); - const gameLatest = path.join(installPath, branch, 'package', 'game', 'latest'); - const userDataPath = findUserDataPath(gameLatest); + // NEW 2.1.2: Use centralized UserData location + const userDataPath = getHytaleSavesDir(); const profilesDir = path.join(userDataPath, 'Profiles'); if (!fs.existsSync(profilesDir)) { @@ -247,6 +250,7 @@ function getProfilesDir(customInstallPath = null) { module.exports = { getAppDir, + getHytaleSavesDir, getResolvedAppDir, expandHome, APP_DIR, diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js index 6a7a379..fddfa7f 100644 --- a/backend/managers/gameLauncher.js +++ b/backend/managers/gameLauncher.js @@ -12,6 +12,7 @@ const { resolveJavaPath, getJavaExec, getBundledJavaPath, detectSystemJava, JAVA const { getLatestClientVersion } = require('../services/versionManager'); const { updateGameFiles } = require('./gameManager'); const { syncModsForCurrentProfile } = require('./modManager'); +const { getUserDataPath } = require('../utils/userDataMigration'); // Client patcher for custom auth server (sanasol.ws) let clientPatcher = null; @@ -106,7 +107,9 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr const customAppDir = getResolvedAppDir(installPathOverride); 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'); + + // NEW 2.1.2: Use centralized UserData location + const userDataDir = getUserDataPath(); const gameLatest = customGameDir; let clientPath = findClientPath(gameLatest); diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index 963bbdd..2f164bd 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -9,7 +9,7 @@ const { installButler } = require('./butlerManager'); const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager'); 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'); +const { getUserDataPath, migrateUserDataToCentralized } = require('../utils/userDataMigration'); async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) { const osName = getOS(); @@ -308,31 +308,25 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir 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); - const oldBranch = config.version_branch || 'release'; // L'ancienne branche pour le backup - console.log(`[UpdateGameFiles] hasVersionConfig: ${hasVersionConfig}`); + const oldBranch = config.version_branch || 'release'; console.log(`[UpdateGameFiles] Switching from ${oldBranch} to ${branch}`); try { - if (progressCallback) { - progressCallback('Backing up user data...', 5, null, null, null); - } - - // Backup UserData AVANT de télécharger/installer (critical for same-branch updates) + // NEW 2.1.2: Ensure UserData migration to centralized location try { - console.log(`[UpdateGameFiles] Attempting to backup UserData from old branch: ${oldBranch}`); - backupPath = await userDataBackup.backupUserData(installPath, oldBranch, hasVersionConfig); - if (backupPath) { - console.log(`[UpdateGameFiles] ✓ UserData backed up from ${oldBranch}: ${backupPath}`); + console.log('[UpdateGameFiles] Ensuring UserData migration...'); + const migrationResult = await migrateUserDataToCentralized(); + if (migrationResult.migrated) { + console.log('[UpdateGameFiles] ✓ UserData migrated to centralized location'); + } else if (migrationResult.alreadyMigrated) { + console.log('[UpdateGameFiles] ✓ UserData already in centralized location'); } - } catch (backupError) { - console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message); + } catch (migrationError) { + console.warn('[UpdateGameFiles] UserData migration warning:', migrationError.message); } if (progressCallback) { @@ -390,31 +384,9 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, const logoResult = await downloadAndReplaceLogo(gameDir, progressCallback); console.log('Logo@2x.png update result after update:', logoResult); - // Ensure UserData directory exists - const userDataDir = path.join(gameDir, 'Client', 'UserData'); - if (!fs.existsSync(userDataDir)) { - console.log(`[UpdateGameFiles] Creating UserData directory at: ${userDataDir}`); - fs.mkdirSync(userDataDir, { recursive: true }); - } - - if (progressCallback) { - progressCallback('Restoring user data...', 90, null, null, null); - } - - // Restore UserData using new system - if (backupPath) { - try { - console.log(`[UpdateGameFiles] Restoring UserData from ${oldBranch} to ${branch}`); - console.log(`[UpdateGameFiles] Source backup: ${backupPath}`); - await userDataBackup.restoreUserData(backupPath, installPath, branch); - await userDataBackup.cleanupBackup(backupPath); - console.log(`[UpdateGameFiles] ✓ UserData migrated successfully from ${oldBranch} to ${branch}`); - } catch (restoreError) { - console.warn('[UpdateGameFiles] ✗ UserData restore failed:', restoreError.message); - } - } else { - console.log('[UpdateGameFiles] No backup to restore, empty UserData folder created'); - } + // NEW 2.1.2: No longer create UserData in game installation + // UserData is now in centralized location (getUserDataPath()) + console.log('[UpdateGameFiles] UserData is managed in centralized location'); console.log(`Game files updated successfully to version: ${newVersion}`); @@ -434,15 +406,6 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, } catch (error) { console.error('Error updating game files:', error); - if (backupPath) { - try { - await userDataBackup.cleanupBackup(backupPath); - console.log('UserData backup cleaned up after error'); - } catch (cleanupError) { - console.warn('Could not clean up UserData backup:', cleanupError.message); - } - } - if (tempUpdateDir && fs.existsSync(tempUpdateDir)) { fs.rmSync(tempUpdateDir, { recursive: true, force: true }); } @@ -470,28 +433,18 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver const customToolsDir = path.join(customAppDir, 'butler'); 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 detected - 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); - } + // NEW 2.1.2: Ensure UserData migration to centralized location try { - console.log(`[InstallGame] Attempting UserData backup (hasVersionConfig: ${hasVersionConfig})...`); - backupPath = await userDataBackup.backupUserData(customAppDir, branch, hasVersionConfig); - if (backupPath) { - console.log(`[InstallGame] ✓ UserData backed up to: ${backupPath}`); + console.log('[InstallGame] Ensuring UserData migration...'); + const migrationResult = await migrateUserDataToCentralized(); + if (migrationResult.migrated) { + console.log('[InstallGame] ✓ UserData migrated to centralized location'); + } else if (migrationResult.alreadyMigrated) { + console.log('[InstallGame] ✓ UserData already in centralized location'); } - } catch (backupError) { - console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message); + } catch (migrationError) { + console.warn('[InstallGame] UserData migration warning:', migrationError.message); } [customAppDir, customCacheDir, customToolsDir].forEach(dir => { @@ -500,10 +453,6 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver } }); - if (!fs.existsSync(userDataDir)) { - fs.mkdirSync(userDataDir, { recursive: true }); - } - saveUsername(playerName); if (installPathOverride) { saveInstallPath(installPathOverride); @@ -595,29 +544,9 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver 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] Creating UserData directory at: ${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] Restoring UserData from: ${backupPath}`); - await userDataBackup.restoreUserData(backupPath, customAppDir, branch); - await userDataBackup.cleanupBackup(backupPath); - console.log('[InstallGame] ✓ UserData restored successfully'); - } catch (restoreError) { - console.warn('[InstallGame] ✗ UserData restore failed:', restoreError.message); - } - } else { - console.log('[InstallGame] No backup to restore, empty UserData folder created'); - } + // NEW 2.1.2: No longer create UserData in game installation + // UserData is managed in centralized location (getUserDataPath()) + console.log('[InstallGame] UserData is managed in centralized location'); if (progressCallback) { progressCallback('Installation complete', 100, null, null, null); diff --git a/backend/utils/userDataBackup.js b/backend/utils/userDataBackup.js index 0da8614..9380d1c 100644 --- a/backend/utils/userDataBackup.js +++ b/backend/utils/userDataBackup.js @@ -46,7 +46,8 @@ class UserDataBackup { console.log(`[UserDataBackup] Copying from ${userDataPath} to ${backupPath}...`); await fs.copy(userDataPath, backupPath, { overwrite: true, - errorOnExist: false + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows }); console.log('[UserDataBackup] ✓ Backup completed successfully'); return backupPath; @@ -82,7 +83,8 @@ class UserDataBackup { await fs.copy(backupPath, userDataPath, { overwrite: true, - errorOnExist: false + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows }); console.log('UserData restore completed successfully'); diff --git a/backend/utils/userDataMigration.js b/backend/utils/userDataMigration.js new file mode 100644 index 0000000..57e7332 --- /dev/null +++ b/backend/utils/userDataMigration.js @@ -0,0 +1,172 @@ +const fs = require('fs-extra'); +const path = require('path'); +const { getHytaleSavesDir, getResolvedAppDir } = require('../core/paths'); +const { loadConfig, saveConfig } = require('../core/config'); + +/** + * NEW SYSTEM (2.1.2+): UserData Migration to Centralized Location + * + * UserData is now stored in a centralized location instead of inside game installation: + * - Windows: %LOCALAPPDATA%\HytaleSaves\ + * - macOS: ~/Library/Application Support/HytaleSaves/ + * - Linux: ~/.hytalesaves/ + * + * This eliminates the need for backup/restore during updates. + */ + +/** + * Check if migration to centralized UserData has been completed + */ +function isMigrationCompleted() { + const config = loadConfig(); + return config.userDataMigrated === true; +} + +/** + * Mark migration as completed + */ +function markMigrationCompleted() { + saveConfig({ userDataMigrated: true }); + console.log('[UserDataMigration] Migration marked as completed in config'); +} + +/** + * Find old UserData location (pre-2.1.2) + * Searches in: installPath/branch/package/game/latest/Client/UserData + */ +function findOldUserDataPath() { + try { + const config = loadConfig(); + const installPath = getResolvedAppDir(); + const branch = config.version_branch || 'release'; + + console.log(`[UserDataMigration] Looking for old UserData...`); + console.log(`[UserDataMigration] Install path: ${installPath}`); + console.log(`[UserDataMigration] Branch: ${branch}`); + + // Old location + const oldPath = path.join(installPath, branch, 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataMigration] Checking: ${oldPath}`); + console.log(`[UserDataMigration] Checking: ${oldPath}`); + + if (fs.existsSync(oldPath)) { + console.log(`[UserDataMigration] ✓ Found old UserData at: ${oldPath}`); + return oldPath; + } + + console.log(`[UserDataMigration] ✗ Not found at current branch location`); + + // Try other branch if current doesn't exist + const otherBranch = branch === 'release' ? 'pre-release' : 'release'; + const otherPath = path.join(installPath, otherBranch, 'package', 'game', 'latest', 'Client', 'UserData'); + console.log(`[UserDataMigration] Checking other branch: ${otherPath}`); + console.log(`[UserDataMigration] Checking other branch: ${otherPath}`); + + if (fs.existsSync(otherPath)) { + console.log(`[UserDataMigration] ✓ Found old UserData in other branch at: ${otherPath}`); + return otherPath; + } + + console.log('[UserDataMigration] ✗ No old UserData found in any branch'); + return null; + } catch (error) { + console.error('[UserDataMigration] Error finding old UserData:', error); + return null; + } +} + +/** + * Migrate UserData from old location to new centralized location + * One-time operation when upgrading to 2.1.2 + */ +async function migrateUserDataToCentralized() { + // Check if already migrated + if (isMigrationCompleted()) { + console.log('[UserDataMigration] Migration already completed, skipping'); + return { success: true, alreadyMigrated: true }; + } + + console.log('[UserDataMigration] === Starting UserData Migration to Centralized Location ==='); + + const newUserDataPath = getHytaleSavesDir(); + console.log(`[UserDataMigration] Target location: ${newUserDataPath}`); + + // Ensure new directory exists + if (!fs.existsSync(newUserDataPath)) { + fs.mkdirSync(newUserDataPath, { recursive: true }); + console.log('[UserDataMigration] Created new HytaleSaves directory'); + } + + // Find old UserData + const oldUserDataPath = findOldUserDataPath(); + + if (!oldUserDataPath) { + console.log('[UserDataMigration] No old UserData found - fresh install or already migrated'); + // Don't mark as migrated - let it check again next time in case game gets installed later + return { success: true, freshInstall: true }; + } + + // Check if new location already has data (shouldn't happen, but safety check) + const existingFiles = fs.readdirSync(newUserDataPath); + if (existingFiles.length > 0) { + console.warn('[UserDataMigration] New location already contains files, marking as migrated to avoid re-attempts'); + markMigrationCompleted(); + return { success: true, skipped: true, reason: 'target_not_empty' }; + } + + try { + console.log(`[UserDataMigration] Copying from ${oldUserDataPath} to ${newUserDataPath}...`); + + // Copy all UserData to new location + await fs.copy(oldUserDataPath, newUserDataPath, { + overwrite: false, + errorOnExist: false, + dereference: true // Follow symlinks to avoid EPERM errors on Windows + }); + + console.log('[UserDataMigration] ✓ UserData copied successfully'); + + // Mark migration as completed + markMigrationCompleted(); + + console.log('[UserDataMigration] === Migration Completed Successfully ==='); + return { + success: true, + migrated: true, + from: oldUserDataPath, + to: newUserDataPath + }; + + } catch (error) { + console.error('[UserDataMigration] ✗ Migration failed:', error); + return { + success: false, + error: error.message, + from: oldUserDataPath, + to: newUserDataPath + }; + } +} + +/** + * Get the centralized UserData path (always use this in 2.1.2+) + * Ensures directory exists + */ +function getUserDataPath() { + const userDataPath = getHytaleSavesDir(); + + // Ensure directory exists + if (!fs.existsSync(userDataPath)) { + fs.mkdirSync(userDataPath, { recursive: true }); + console.log(`[UserDataMigration] Created UserData directory: ${userDataPath}`); + } + + return userDataPath; +} + +module.exports = { + migrateUserDataToCentralized, + getUserDataPath, + isMigrationCompleted, + findOldUserDataPath +}; diff --git a/main.js b/main.js index 2d8c8f4..0aad11f 100644 --- a/main.js +++ b/main.js @@ -5,6 +5,7 @@ const { autoUpdater } = require('electron-updater'); const fs = require('fs'); const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, saveLanguage, loadLanguage, saveCloseLauncherOnStart, loadCloseLauncherOnStart, saveLauncherHardwareAcceleration, loadLauncherHardwareAcceleration, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const { retryPWRDownload } = require('./backend/managers/gameManager'); +const { migrateUserDataToCentralized } = require('./backend/utils/userDataMigration'); // Handle Hardware Acceleration try { @@ -298,6 +299,14 @@ app.whenReady().then(async () => { // Initialize Profile Manager (runs migration if needed) profileManager.init(); + // Migrate UserData to centralized location (v2.1.2+) + console.log('[Startup] Checking UserData migration...'); + try { + await migrateUserDataToCentralized(); + } catch (error) { + console.error('[Startup] UserData migration failed:', error); + } + createSplashScreen(); setTimeout(async () => { diff --git a/package.json b/package.json index 83a319b..c496c76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hytale-f2p-launcher", - "version": "2.1.1", + "version": "2.1.2", "description": "A modern, cross-platform launcher for Hytale with automatic updates and multi-client support", "homepage": "https://github.com/amiayweb/Hytale-F2P", "main": "main.js",