Files
hytale-f2p/backend/managers/gameManager.js
2026-01-24 13:44:56 +01:00

562 lines
19 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const { execFile } = require('child_process');
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
const { getOS, getArch } = require('../utils/platformUtils');
const { downloadFile } = require('../utils/fileManager');
const { getLatestClientVersion } = require('../services/versionManager');
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');
async function downloadPWR(branch = 'release', fileName = '4.pwr', progressCallback, cacheDir = CACHE_DIR) {
const osName = getOS();
const arch = getArch();
if (osName === 'darwin' && arch === 'amd64') {
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}/${branch}/0/${fileName}`;
const dest = path.join(cacheDir, `${branch}_${fileName}`);
// Check if file exists and validate it
if (fs.existsSync(dest)) {
console.log('PWR file found in cache:', dest);
// Validate file size (PWR files should be > 1MB)
const stats = fs.statSync(dest);
if (stats.size < 1024 * 1024) {
console.log('Cached PWR file seems corrupted (too small), re-downloading...');
fs.unlinkSync(dest);
} else {
return dest;
}
}
console.log('Fetching PWR patch file:', url);
await downloadFile(url, dest, progressCallback);
// Validate downloaded file
const stats = fs.statSync(dest);
console.log(`PWR file downloaded, size: ${(stats.size / 1024 / 1024).toFixed(2)} MB`);
if (stats.size < 1024 * 1024) {
fs.unlinkSync(dest);
throw new Error('Downloaded PWR file is corrupted (file too small)');
}
console.log('PWR saved to:', dest);
return dest;
}
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR) {
const butlerPath = await installButler(toolsDir);
const gameLatest = gameDir;
const stagingDir = path.join(gameLatest, 'staging-temp');
const clientPath = findClientPath(gameLatest);
if (clientPath) {
console.log('Game files detected, skipping patch installation.');
return;
}
if (!fs.existsSync(gameLatest)) {
fs.mkdirSync(gameLatest, { recursive: true });
}
if (!fs.existsSync(stagingDir)) {
fs.mkdirSync(stagingDir, { recursive: true });
}
if (progressCallback) {
progressCallback('Installing game patch...', null, null, null, null);
}
console.log('Installing game patch...');
if (!fs.existsSync(butlerPath)) {
throw new Error(`Butler tool not found at: ${butlerPath}`);
}
if (!fs.existsSync(pwrFile)) {
throw new Error(`PWR file not found at: ${pwrFile}`);
}
const args = [
'apply',
'--staging-dir',
stagingDir,
pwrFile,
gameLatest
];
try {
await new Promise((resolve, reject) => {
const child = execFile(butlerPath, args, {
maxBuffer: 1024 * 1024 * 10,
timeout: 600000
}, (error, stdout, stderr) => {
if (error) {
console.error('Butler stderr:', stderr);
console.error('Butler stdout:', stdout);
// Check for EOF error (corrupted PWR file)
if (stderr && stderr.includes('unexpected EOF')) {
// Delete corrupted PWR file
console.log('Corrupted PWR file detected, deleting:', pwrFile);
try {
if (fs.existsSync(pwrFile)) {
fs.unlinkSync(pwrFile);
console.log('Corrupted PWR file deleted. Please try again to re-download.');
}
} catch (delErr) {
console.error('Failed to delete corrupted PWR file:', delErr);
}
reject(new Error(`Corrupted PWR file detected and deleted. Please try launching the game again.`));
} else {
reject(new Error(`Patch installation failed: ${error.message}${stderr ? '\n' + stderr : ''}`));
}
} else {
resolve();
}
});
});
} catch (error) {
throw error;
}
if (fs.existsSync(stagingDir)) {
fs.rmSync(stagingDir, { recursive: true, force: true });
}
if (progressCallback) {
progressCallback('Installation complete', null, null, null, null);
}
console.log('Installation complete');
}
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}`);
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)
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}`);
}
} catch (backupError) {
console.warn('[UpdateGameFiles] ✗ UserData backup failed:', backupError.message);
}
if (progressCallback) {
progressCallback('Updating game files...', 10, null, null, null);
}
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
if (fs.existsSync(tempUpdateDir)) {
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
}
fs.mkdirSync(tempUpdateDir, { recursive: true });
if (progressCallback) {
progressCallback('Downloading new game version...', 20, null, null, null);
}
const pwrFile = await downloadPWR(branch, newVersion, progressCallback, cacheDir);
if (progressCallback) {
progressCallback('Extracting new files...', 60, null, null, null);
}
await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir);
if (progressCallback) {
progressCallback('Replacing game files...', 80, null, null, null);
}
if (fs.existsSync(gameDir)) {
console.log('Removing old game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
}
fs.renameSync(tempUpdateDir, gameDir);
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
console.log('HomePage.ui update result after update:', homeUIResult);
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');
}
console.log(`Game files updated successfully to version: ${newVersion}`);
// 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));
if (progressCallback) {
progressCallback('Game update completed', 100, null, null, null);
}
return { success: true, updated: true, version: newVersion };
} 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 });
}
throw new Error(`Failed to update game files: ${error.message}`);
}
}
function isGameInstalled(branchOverride = null) {
const branch = branchOverride || loadVersionBranch();
const appDir = getResolvedAppDir();
const gameDir = path.join(appDir, branch, 'package', 'game', 'latest');
const clientPath = findClientPath(gameDir);
return clientPath !== null;
}
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, 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);
}
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}`);
}
} catch (backupError) {
console.warn('[InstallGame] ✗ UserData backup failed:', backupError.message);
}
[customAppDir, customCacheDir, customToolsDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
if (!fs.existsSync(userDataDir)) {
fs.mkdirSync(userDataDir, { recursive: true });
}
saveUsername(playerName);
if (installPathOverride) {
saveInstallPath(installPathOverride);
}
const gameLatest = customGameDir;
let clientPath = findClientPath(gameLatest);
if (clientPath) {
if (progressCallback) {
progressCallback('Game already installed', 100, null, null, null);
}
console.log('Game is already installed');
return { success: true, alreadyInstalled: true };
}
const configuredJava = (javaPathOverride !== undefined && javaPathOverride !== null
? javaPathOverride
: loadJavaPath() || '').trim();
let javaBin = null;
if (configuredJava) {
javaBin = await resolveJavaPath(configuredJava);
if (!javaBin) {
throw new Error(`Configured Java path not found: ${configuredJava}`);
}
} else {
try {
await downloadJRE(progressCallback, customCacheDir, customJreDir);
} catch (error) {
const fallback = await detectSystemJava();
if (fallback) {
javaBin = fallback;
} else {
throw error;
}
}
if (!javaBin) {
javaBin = getJavaExec(customJreDir);
}
}
if (progressCallback) {
progressCallback('Fetching game files...', null, null, null, null);
}
console.log(`Installing game files for branch: ${branch}...`);
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] 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');
}
if (progressCallback) {
progressCallback('Installation complete', 100, null, null, null);
}
console.log('Game installation completed successfully');
return {
success: true,
installed: true
};
}
async function uninstallGame() {
const appDir = getResolvedAppDir();
if (!fs.existsSync(appDir)) {
throw new Error('Game is not installed');
}
try {
fs.rmSync(appDir, { recursive: true, force: true });
console.log('Game uninstalled successfully - removed entire HytaleF2P folder');
if (fs.existsSync(CONFIG_FILE)) {
const config = loadConfig();
delete config.installPath;
fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2), 'utf8');
}
} catch (error) {
throw new Error(`Failed to uninstall game: ${error.message}`);
}
}
function checkExistingGameInstallation(branchOverride = null) {
try {
const branch = branchOverride || loadVersionBranch();
const config = loadConfig();
if (!config.installPath || !config.installPath.trim()) {
return null;
}
const installPath = config.installPath.trim();
const gameDir = path.join(installPath, 'HytaleF2P', branch, 'package', 'game', 'latest');
if (!fs.existsSync(gameDir)) {
return null;
}
const clientPath = findClientPath(gameDir);
if (!clientPath) {
return null;
}
const userDataPath = findUserDataRecursive(gameDir);
return {
gameDir: gameDir,
clientPath: clientPath,
userDataPath: userDataPath,
installPath: installPath,
hasUserData: userDataPath && fs.existsSync(userDataPath),
branch: branch
};
} catch (error) {
console.error('Error checking existing game installation:', error);
return null;
}
}
async function repairGame(progressCallback, branchOverride = null) {
const branch = branchOverride || loadVersionBranch();
const appDir = getResolvedAppDir();
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.');
}
if (progressCallback) {
progressCallback('Backing up user data...', 10, null, null, null);
}
// 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) {
progressCallback('Removing old game files...', 30, null, null, null);
}
// Delete Game and Cache Directory
console.log('Removing corrupted game files...');
fs.rmSync(gameDir, { recursive: true, force: true });
const cacheDir = path.join(appDir, 'cache');
if (fs.existsSync(cacheDir)) {
console.log('Clearing cache directory...');
fs.rmSync(cacheDir, { recursive: true, force: true });
}
console.log('Reinstalling game files...');
// Passing null/undefined for overrides to use defaults/saved configs
// installGame calls progressCallback internally
await installGame('Player', progressCallback, null, null, branch);
// Restore UserData using new system
if (backupPath) {
if (progressCallback) {
progressCallback('Restoring user data...', 90, null, null, null);
}
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);
}
}
if (progressCallback) {
progressCallback('Repair completed successfully!', 100, null, null, null);
}
return { success: true, repaired: true };
}
module.exports = {
downloadPWR,
applyPWR,
updateGameFiles,
isGameInstalled,
installGame,
uninstallGame,
isGameInstalled,
installGame,
uninstallGame,
checkExistingGameInstallation,
repairGame
};