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:
@@ -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