mirror of
https://git.sanhost.net/sanasol/hytale-f2p.git
synced 2026-02-26 06:41:47 -03:00
v2.3.1: CDN redirect gateway, fix token username bug
- Migrate patch downloads to auth server redirect gateway (302 -> CDN) Allows instant CDN switching via admin panel without launcher update - Fix identity token "Player" username mismatch on fresh install Add token username verification with retry in fetchAuthTokens - Refactor versionManager to use mirror manifest via auth.sanasol.ws/patches - Add optimal patch routing (BFS) for differential updates - Add PATCH_CDN_INFRASTRUCTURE.md documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@ const path = require('path');
|
||||
const { execFile } = require('child_process');
|
||||
const { downloadFile, retryDownload } = require('../utils/fileManager');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { validateChecksum, extractVersionDetails, canUseDifferentialUpdate, needsIntermediatePatches, getInstalledClientVersion } = require('../services/versionManager');
|
||||
const { validateChecksum, extractVersionDetails, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
||||
const { installButler } = require('./butlerManager');
|
||||
const { GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||
const { saveVersionClient } = require('../core/config');
|
||||
@@ -30,7 +30,7 @@ async function acquireGameArchive(downloadUrl, targetPath, checksum, progressCal
|
||||
}
|
||||
|
||||
console.log(`Downloading game archive from: ${downloadUrl}`);
|
||||
|
||||
|
||||
try {
|
||||
if (allowRetry) {
|
||||
await retryDownload(downloadUrl, targetPath, progressCallback);
|
||||
@@ -103,13 +103,13 @@ async function deployGameArchive(archivePath, destinationDir, toolsDir, progress
|
||||
if (error) {
|
||||
const cleanStderr = stderr.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||
const cleanStdout = stdout.replace(/[\u2714\u2716\u2713\u2717\u26A0\uD83D[\uDC00-\uDFFF]]/g, '').trim();
|
||||
|
||||
|
||||
if (cleanStderr) console.error('Deployment stderr:', cleanStderr);
|
||||
if (cleanStdout) console.error('Deployment stdout:', cleanStdout);
|
||||
|
||||
|
||||
const errorText = (stderr + ' ' + error.message).toLowerCase();
|
||||
let message = 'Game deployment failed';
|
||||
|
||||
|
||||
if (errorText.includes('unexpected eof')) {
|
||||
message = 'Corrupted archive detected. Please retry download.';
|
||||
if (fs.existsSync(archivePath)) {
|
||||
@@ -156,20 +156,20 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
console.log(`Initiating intelligent update to version ${targetVersion}`);
|
||||
|
||||
const currentVersion = getInstalledClientVersion();
|
||||
console.log(`Current version: ${currentVersion || 'none (clean install)'}`);
|
||||
console.log(`Target version: ${targetVersion}`);
|
||||
console.log(`Branch: ${branch}`);
|
||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||
const targetBuild = extractVersionNumber(targetVersion);
|
||||
console.log(`Current build: ${currentBuild}, Target build: ${targetBuild}, Branch: ${branch}`);
|
||||
|
||||
// For non-release branches, always do full install
|
||||
if (branch !== 'release') {
|
||||
console.log(`Pre-release branch detected - forcing full archive download`);
|
||||
console.log('Pre-release branch detected - forcing full archive download');
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Downloading full game archive (pre-release)...', 0, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
@@ -177,16 +177,16 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentVersion) {
|
||||
// Clean install (no current version)
|
||||
if (currentBuild === 0) {
|
||||
console.log('No existing installation detected - downloading full archive');
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full game archive (first install - v${targetVersion})...`, 0, null, null, null);
|
||||
progressCallback(`Downloading full game archive (first install - v${targetBuild})...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
@@ -194,59 +194,67 @@ async function performIntelligentUpdate(targetVersion, branch = 'release', progr
|
||||
return;
|
||||
}
|
||||
|
||||
const patchesToApply = needsIntermediatePatches(currentVersion, targetVersion);
|
||||
|
||||
if (patchesToApply.length === 0) {
|
||||
console.log('Already at target version or invalid version sequence');
|
||||
// Already at target
|
||||
if (currentBuild >= targetBuild) {
|
||||
console.log('Already at target version or newer');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Applying ${patchesToApply.length} differential patch(es): ${patchesToApply.join(' -> ')}`);
|
||||
// Use mirror's update plan for optimal patch routing
|
||||
try {
|
||||
const plan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||
|
||||
console.log(`Applying ${plan.steps.length} patch(es): ${plan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')}`);
|
||||
|
||||
for (let i = 0; i < plan.steps.length; i++) {
|
||||
const step = plan.steps[i];
|
||||
const stepName = `${step.from}_to_${step.to}`;
|
||||
const archivePath = path.join(cacheDir, `${branch}_${stepName}.pwr`);
|
||||
const isDifferential = step.from !== 0;
|
||||
|
||||
for (let i = 0; i < patchesToApply.length; i++) {
|
||||
const patchVersion = patchesToApply[i];
|
||||
const versionDetails = await extractVersionDetails(patchVersion, branch);
|
||||
|
||||
const canDifferential = canUseDifferentialUpdate(getInstalledClientVersion(), versionDetails);
|
||||
|
||||
if (!canDifferential || !versionDetails.differentialUrl) {
|
||||
console.log(`WARNING: Differential patch not available for ${patchVersion}, using full archive`);
|
||||
const archiveName = path.basename(versionDetails.fullUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_${archiveName}`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full archive for ${patchVersion} (${i + 1}/${patchesToApply.length})...`, 0, null, null, null);
|
||||
progressCallback(`Downloading patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
} else {
|
||||
console.log(`Applying differential patch: ${versionDetails.sourceVersion} -> ${patchVersion}`);
|
||||
const archiveName = path.basename(versionDetails.differentialUrl);
|
||||
const archivePath = path.join(cacheDir, `${branch}_patch_${archiveName}`);
|
||||
|
||||
|
||||
await acquireGameArchive(step.url, archivePath, null, progressCallback);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Applying patch ${i + 1}/${patchesToApply.length}: ${patchVersion}...`, 0, null, null, null);
|
||||
progressCallback(`Applying patch ${i + 1}/${plan.steps.length}: ${stepName}...`, 50, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.differentialUrl, archivePath, versionDetails.checksum, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, true);
|
||||
|
||||
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, isDifferential);
|
||||
|
||||
// Clean up patch file
|
||||
if (fs.existsSync(archivePath)) {
|
||||
try {
|
||||
fs.unlinkSync(archivePath);
|
||||
console.log(`Cleaned up patch file: ${archiveName}`);
|
||||
console.log(`Cleaned up: ${stepName}.pwr`);
|
||||
} catch (cleanupErr) {
|
||||
console.warn(`Failed to cleanup patch file: ${cleanupErr.message}`);
|
||||
console.warn(`Failed to cleanup: ${cleanupErr.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
saveVersionClient(patchVersion);
|
||||
console.log(`Patch ${patchVersion} applied successfully (${i + 1}/${patchesToApply.length})`);
|
||||
}
|
||||
|
||||
console.log(`Update completed successfully. Version ${targetVersion} is now installed.`);
|
||||
saveVersionClient(`v${step.to}`);
|
||||
console.log(`Patch ${stepName} applied (${i + 1}/${plan.steps.length})`);
|
||||
}
|
||||
|
||||
console.log(`Update completed. Version ${targetVersion} is now installed.`);
|
||||
} catch (planError) {
|
||||
console.error('Update plan failed:', planError.message);
|
||||
console.log('Falling back to full archive download');
|
||||
|
||||
// Fallback: full install
|
||||
const versionDetails = await extractVersionDetails(targetVersion, branch);
|
||||
const archivePath = path.join(cacheDir, `${branch}_0_to_${targetBuild}.pwr`);
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Downloading full game archive (fallback)...`, 0, null, null, null);
|
||||
}
|
||||
|
||||
await acquireGameArchive(versionDetails.fullUrl, archivePath, null, progressCallback);
|
||||
await deployGameArchive(archivePath, gameDir, toolsDir, progressCallback, false);
|
||||
saveVersionClient(targetVersion);
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureGameInstalled(targetVersion, branch = 'release', progressCallback, gameDir = GAME_DIR, cacheDir = CACHE_DIR, toolsDir = TOOLS_DIR) {
|
||||
|
||||
@@ -61,12 +61,39 @@ async function fetchAuthTokens(uuid, name) {
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('Auth tokens received from server');
|
||||
const identityToken = data.IdentityToken || data.identityToken;
|
||||
const sessionToken = data.SessionToken || data.sessionToken;
|
||||
|
||||
return {
|
||||
identityToken: data.IdentityToken || data.identityToken,
|
||||
sessionToken: data.SessionToken || data.sessionToken
|
||||
};
|
||||
// Verify the identity token has the correct username
|
||||
// This catches cases where the auth server defaults to "Player"
|
||||
try {
|
||||
const parts = identityToken.split('.');
|
||||
if (parts.length >= 2) {
|
||||
const payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString());
|
||||
if (payload.username && payload.username !== name && name !== 'Player') {
|
||||
console.warn(`[Auth] Token username mismatch: token has "${payload.username}", expected "${name}". Retrying...`);
|
||||
// Retry once with explicit name
|
||||
const retryResponse = await fetch(`${authServerUrl}/game-session/child`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ uuid: uuid, name: name, scopes: ['hytale:server', 'hytale:client'] })
|
||||
});
|
||||
if (retryResponse.ok) {
|
||||
const retryData = await retryResponse.json();
|
||||
console.log('[Auth] Retry successful');
|
||||
return {
|
||||
identityToken: retryData.IdentityToken || retryData.identityToken,
|
||||
sessionToken: retryData.SessionToken || retryData.sessionToken
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (verifyErr) {
|
||||
console.warn('[Auth] Token verification skipped:', verifyErr.message);
|
||||
}
|
||||
|
||||
console.log('Auth tokens received from server');
|
||||
return { identityToken, sessionToken };
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch auth tokens:', error.message);
|
||||
// Fallback to local generation if server unavailable
|
||||
|
||||
@@ -5,7 +5,7 @@ const { promisify } = require('util');
|
||||
const { getResolvedAppDir, findClientPath, findUserDataPath, findUserDataRecursive, GAME_DIR, CACHE_DIR, TOOLS_DIR } = require('../core/paths');
|
||||
const { getOS, getArch } = require('../utils/platformUtils');
|
||||
const { downloadFile, retryDownload, retryStalledDownload, MAX_AUTOMATIC_STALL_RETRIES } = require('../utils/fileManager');
|
||||
const { getLatestClientVersion, getInstalledClientVersion } = require('../services/versionManager');
|
||||
const { getLatestClientVersion, getInstalledClientVersion, getUpdatePlan, extractVersionNumber } = require('../services/versionManager');
|
||||
const { FORCE_CLEAN_INSTALL_VERSION, CLEAN_INSTALL_TEST_VERSION } = require('../core/testConfig');
|
||||
const { installButler } = require('./butlerManager');
|
||||
const { downloadAndReplaceHomePageUI, downloadAndReplaceLogo } = require('./uiFileManager');
|
||||
@@ -64,7 +64,7 @@ async function safeRemoveDirectory(dirPath, maxRetries = 3) {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false) {
|
||||
async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback, cacheDir = CACHE_DIR, manualRetry = false, directUrl = null) {
|
||||
const osName = getOS();
|
||||
const arch = getArch();
|
||||
|
||||
@@ -72,43 +72,38 @@ async function downloadPWR(branch = 'release', fileName = 'v8', progressCallback
|
||||
throw new Error('Hytale x86_64 Intel Mac Support has not been released yet. Please check back later.');
|
||||
}
|
||||
|
||||
const { getPWRUrlFromNewAPI } = require('../services/versionManager');
|
||||
|
||||
let url;
|
||||
let isUsingNewAPI = false;
|
||||
|
||||
try {
|
||||
console.log(`[DownloadPWR] Fetching URL from new API for branch: ${branch}, version: ${fileName}`);
|
||||
url = await getPWRUrlFromNewAPI(branch, fileName);
|
||||
isUsingNewAPI = true;
|
||||
console.log(`[DownloadPWR] Using new API URL: ${url}`);
|
||||
} catch (error) {
|
||||
console.error(`[DownloadPWR] Failed to get URL from new API: ${error.message}`);
|
||||
console.log(`[DownloadPWR] Falling back to old URL format`);
|
||||
url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${branch}/0/${fileName}.pwr`;
|
||||
|
||||
if (directUrl) {
|
||||
url = directUrl;
|
||||
console.log(`[DownloadPWR] Using direct URL: ${url}`);
|
||||
} else {
|
||||
const { getPWRUrl } = require('../services/versionManager');
|
||||
try {
|
||||
console.log(`[DownloadPWR] Fetching mirror URL for branch: ${branch}, version: ${fileName}`);
|
||||
url = await getPWRUrl(branch, fileName);
|
||||
console.log(`[DownloadPWR] Mirror URL: ${url}`);
|
||||
} catch (error) {
|
||||
console.error(`[DownloadPWR] Failed to get mirror URL: ${error.message}`);
|
||||
const { MIRROR_BASE_URL } = require('../services/versionManager');
|
||||
url = `${MIRROR_BASE_URL}/${osName}/${arch}/${branch}/0_to_${extractVersionNumber(fileName)}.pwr`;
|
||||
console.log(`[DownloadPWR] Fallback URL: ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
const dest = path.join(cacheDir, `${branch}_${fileName}.pwr`);
|
||||
|
||||
// Check if file exists and validate it
|
||||
if (fs.existsSync(dest) && !manualRetry) {
|
||||
console.log('PWR file found in cache:', dest);
|
||||
|
||||
// Validate file size (PWR files should be > 1MB and >= 1.5GB for complete downloads)
|
||||
const stats = fs.statSync(dest);
|
||||
if (stats.size < 1024 * 1024) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is under 1.5 GB (incomplete download)
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
if (sizeInMB < 1500) {
|
||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||
return false;
|
||||
if (stats.size > 1024 * 1024) {
|
||||
console.log(`[PWR] Using cached file: ${dest} (${(stats.size / 1024 / 1024).toFixed(2)} MB)`);
|
||||
return dest;
|
||||
}
|
||||
console.log(`[PWR] Cached file too small (${stats.size} bytes), re-downloading`);
|
||||
}
|
||||
|
||||
console.log(`Fetching PWR patch file from ${isUsingNewAPI ? 'NEW API' : 'old API'}:`, url);
|
||||
console.log(`[DownloadPWR] Downloading from: ${url}`);
|
||||
|
||||
try {
|
||||
if (manualRetry) {
|
||||
@@ -203,7 +198,7 @@ async function retryPWRDownload(branch, fileName, progressCallback, cacheDir = C
|
||||
return await downloadPWR(branch, fileName, progressCallback, cacheDir, true);
|
||||
}
|
||||
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR) {
|
||||
async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir = TOOLS_DIR, branch = 'release', cacheDir = CACHE_DIR, skipExistingCheck = false) {
|
||||
console.log(`[Butler] Starting PWR application with:`);
|
||||
console.log(`[Butler] - PWR file: ${pwrFile}`);
|
||||
console.log(`[Butler] - Staging dir: ${path.join(gameDir, 'staging-temp')}`);
|
||||
@@ -227,11 +222,12 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, 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 (!skipExistingCheck) {
|
||||
const clientPath = findClientPath(gameLatest);
|
||||
if (clientPath) {
|
||||
console.log('Game files detected, skipping patch installation.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate and prepare directories
|
||||
@@ -412,57 +408,118 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR,
|
||||
}
|
||||
console.log(`Updating game files to version: ${newVersion} (branch: ${branch})`);
|
||||
|
||||
tempUpdateDir = path.join(gameDir, '..', 'temp_update');
|
||||
// Determine update strategy: intermediate patches vs full reinstall
|
||||
const currentVersion = loadVersionClient();
|
||||
const currentBuild = extractVersionNumber(currentVersion) || 0;
|
||||
const targetBuild = extractVersionNumber(newVersion);
|
||||
|
||||
if (fs.existsSync(tempUpdateDir)) {
|
||||
fs.rmSync(tempUpdateDir, { recursive: true, force: true });
|
||||
}
|
||||
fs.mkdirSync(tempUpdateDir, { recursive: true });
|
||||
let useIntermediatePatches = false;
|
||||
let updatePlan = null;
|
||||
|
||||
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, branch, cacheDir);
|
||||
// Delete PWR file from cache after successful update
|
||||
try {
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||
if (currentBuild > 0 && currentBuild < targetBuild) {
|
||||
try {
|
||||
updatePlan = await getUpdatePlan(currentBuild, targetBuild, branch);
|
||||
useIntermediatePatches = !updatePlan.isFullInstall;
|
||||
if (useIntermediatePatches) {
|
||||
const totalMB = (updatePlan.totalSize / 1024 / 1024).toFixed(0);
|
||||
console.log(`[UpdateGameFiles] Using intermediate patches: ${updatePlan.steps.map(s => `${s.from}\u2192${s.to}`).join(' + ')} (${totalMB} MB)`);
|
||||
}
|
||||
} catch (planError) {
|
||||
console.warn('[UpdateGameFiles] Could not get update plan, falling back to full install:', planError.message);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||
}
|
||||
if (progressCallback) {
|
||||
progressCallback('Replacing game files...', 80, null, null, null);
|
||||
}
|
||||
|
||||
if (fs.existsSync(gameDir)) {
|
||||
console.log('Removing old game files...');
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
if (useIntermediatePatches && updatePlan) {
|
||||
// Apply intermediate patches directly to game dir
|
||||
for (let i = 0; i < updatePlan.steps.length; i++) {
|
||||
const step = updatePlan.steps[i];
|
||||
const stepName = `${step.from}_to_${step.to}`;
|
||||
|
||||
if (progressCallback) {
|
||||
const progress = 20 + Math.round((i / updatePlan.steps.length) * 60);
|
||||
progressCallback(`Downloading patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, progress, null, null, null);
|
||||
}
|
||||
|
||||
const pwrFile = await downloadPWR(branch, stepName, progressCallback, cacheDir, false, step.url);
|
||||
|
||||
if (!pwrFile) {
|
||||
throw new Error(`Failed to download patch ${stepName}`);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback(`Applying patch ${i + 1}/${updatePlan.steps.length} (${stepName})...`, null, null, null, null);
|
||||
}
|
||||
|
||||
await applyPWR(pwrFile, progressCallback, gameDir, toolsDir, branch, cacheDir, true);
|
||||
|
||||
// Clean up PWR file from cache
|
||||
try {
|
||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||
break;
|
||||
} catch (err) {
|
||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||
retries--;
|
||||
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
throw err;
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR from cache:', delErr.message);
|
||||
}
|
||||
|
||||
// Save intermediate version so we can resume if interrupted
|
||||
saveVersionClient(`v${step.to}`);
|
||||
console.log(`[UpdateGameFiles] Applied patch ${stepName} (${i + 1}/${updatePlan.steps.length})`);
|
||||
}
|
||||
} else {
|
||||
// Full install: download 0->target, apply to temp dir, swap
|
||||
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, branch, cacheDir);
|
||||
|
||||
try {
|
||||
if (fs.existsSync(pwrFile)) {
|
||||
fs.unlinkSync(pwrFile);
|
||||
console.log('[UpdateGameFiles] PWR file deleted from cache after successful update:', pwrFile);
|
||||
}
|
||||
} catch (delErr) {
|
||||
console.warn('[UpdateGameFiles] Failed to delete PWR file from cache:', delErr.message);
|
||||
}
|
||||
|
||||
if (progressCallback) {
|
||||
progressCallback('Replacing game files...', 80, null, null, null);
|
||||
}
|
||||
|
||||
if (fs.existsSync(gameDir)) {
|
||||
console.log('Removing old game files...');
|
||||
let retries = 3;
|
||||
while (retries > 0) {
|
||||
try {
|
||||
fs.rmSync(gameDir, { recursive: true, force: true });
|
||||
break;
|
||||
} catch (err) {
|
||||
if ((err.code === 'EPERM' || err.code === 'EBUSY') && retries > 0) {
|
||||
retries--;
|
||||
console.log(`[UpdateGameFiles] Removal failed with ${err.code}, retrying in 1s... (${retries} retries left)`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fs.renameSync(tempUpdateDir, gameDir);
|
||||
fs.renameSync(tempUpdateDir, gameDir);
|
||||
}
|
||||
|
||||
const homeUIResult = await downloadAndReplaceHomePageUI(gameDir, progressCallback);
|
||||
console.log('HomePage.ui update result after update:', homeUIResult);
|
||||
@@ -833,36 +890,23 @@ function validateGameDirectory(gameDir, stagingDir) {
|
||||
}
|
||||
|
||||
// Enhanced PWR file validation
|
||||
// Accepts intermediate patches (50+ MB) and full installs (1.5+ GB)
|
||||
function validatePWRFile(filePath) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
const stats = fs.statSync(filePath);
|
||||
const sizeInMB = stats.size / 1024 / 1024;
|
||||
|
||||
|
||||
// PWR files should be at least 1 MB
|
||||
if (stats.size < 1024 * 1024) {
|
||||
console.log(`[PWR Validation] File too small: ${sizeInMB.toFixed(2)} MB`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if file is under 1.5 GB (incomplete download)
|
||||
if (sizeInMB < 1500) {
|
||||
console.log(`[PWR Validation] File appears incomplete: ${sizeInMB.toFixed(2)} MB < 1.5 GB`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Basic file header validation (PWR files should have specific headers)
|
||||
const buffer = fs.readFileSync(filePath, { start: 0, end: 20 });
|
||||
if (buffer.length < 10) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for common PWR magic bytes or patterns
|
||||
// This is a basic check - could be enhanced with actual PWR format specification
|
||||
const header = buffer.toString('hex', 0, 10);
|
||||
console.log(`[PWR Validation] File header: ${header}`);
|
||||
|
||||
|
||||
console.log(`[PWR Validation] File size: ${sizeInMB.toFixed(2)} MB - OK`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`[PWR Validation] Error:`, error.message);
|
||||
|
||||
Reference in New Issue
Block a user