mirror of
https://git.sanhost.net/sanasol/hytale-f2p
synced 2026-02-26 16:21:49 -03:00
* feat: Add Repair Game functionality including UserData backup and cache clearing * feat: Add In-App Logs Viewer and Logs Folder shortcut * feat: Add Open Logs feature * disable dev tools * Fix Settings UI * fix reorder settings section in index.html relocated sections in settings from most used to least: 1. game options (playername, opengamedir, repair, GPUpreference) 2. player uuid management 3. discord integration rich presence 4. custom java path --------- Co-authored-by: Fazri Gading <super.fai700@gmail.com>
213 lines
6.2 KiB
JavaScript
213 lines
6.2 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const axios = require('axios');
|
|
|
|
async function downloadFile(url, dest, progressCallback, maxRetries = 3) {
|
|
let lastError = null;
|
|
|
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
try {
|
|
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
|
|
|
|
if (attempt > 0 && progressCallback) {
|
|
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null);
|
|
await new Promise(resolve => setTimeout(resolve, 2000 * attempt)); // Délai progressif
|
|
}
|
|
|
|
const response = await axios({
|
|
method: 'GET',
|
|
url: url,
|
|
responseType: 'stream',
|
|
timeout: 60000, // 60 secondes timeout
|
|
headers: {
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
'Accept': '*/*',
|
|
'Accept-Language': 'en-US,en;q=0.9',
|
|
'Referer': 'https://launcher.hytale.com/',
|
|
'Connection': 'keep-alive'
|
|
},
|
|
// Configuration Axios pour la robustesse réseau
|
|
validateStatus: function (status) {
|
|
return status >= 200 && status < 300;
|
|
},
|
|
// Retry configuration
|
|
maxRedirects: 5,
|
|
// Network resilience
|
|
family: 4 // Force IPv4
|
|
});
|
|
|
|
const totalSize = parseInt(response.headers['content-length'], 10);
|
|
let downloaded = 0;
|
|
let lastProgressTime = Date.now();
|
|
const startTime = Date.now();
|
|
|
|
// Nettoyer le fichier de destination s'il existe
|
|
if (fs.existsSync(dest)) {
|
|
fs.unlinkSync(dest);
|
|
}
|
|
|
|
const writer = fs.createWriteStream(dest);
|
|
let downloadStalled = false;
|
|
let stalledTimeout = null;
|
|
|
|
response.data.on('data', (chunk) => {
|
|
downloaded += chunk.length;
|
|
const now = Date.now();
|
|
|
|
// Reset stalled timer on data received
|
|
if (stalledTimeout) {
|
|
clearTimeout(stalledTimeout);
|
|
}
|
|
|
|
// Set new stalled timer (30 seconds without data = stalled)
|
|
stalledTimeout = setTimeout(() => {
|
|
downloadStalled = true;
|
|
writer.destroy();
|
|
response.data.destroy();
|
|
}, 30000);
|
|
|
|
if (progressCallback && totalSize > 0 && (now - lastProgressTime > 100)) { // Update every 100ms max
|
|
const percent = Math.min(100, Math.max(0, (downloaded / totalSize) * 100));
|
|
const elapsed = (now - startTime) / 1000;
|
|
const speed = elapsed > 0 ? downloaded / elapsed : 0;
|
|
progressCallback(null, percent, speed, downloaded, totalSize);
|
|
lastProgressTime = now;
|
|
}
|
|
});
|
|
|
|
response.data.on('error', (error) => {
|
|
if (stalledTimeout) {
|
|
clearTimeout(stalledTimeout);
|
|
}
|
|
console.error(`Stream error on attempt ${attempt + 1}:`, error.code || error.message);
|
|
writer.destroy();
|
|
});
|
|
|
|
response.data.pipe(writer);
|
|
|
|
await new Promise((resolve, reject) => {
|
|
writer.on('finish', () => {
|
|
if (stalledTimeout) {
|
|
clearTimeout(stalledTimeout);
|
|
}
|
|
if (!downloadStalled) {
|
|
console.log(`Download completed successfully on attempt ${attempt + 1}`);
|
|
resolve();
|
|
} else {
|
|
reject(new Error('Download stalled'));
|
|
}
|
|
});
|
|
|
|
writer.on('error', (error) => {
|
|
if (stalledTimeout) {
|
|
clearTimeout(stalledTimeout);
|
|
}
|
|
reject(error);
|
|
});
|
|
|
|
response.data.on('error', (error) => {
|
|
if (stalledTimeout) {
|
|
clearTimeout(stalledTimeout);
|
|
}
|
|
reject(error);
|
|
});
|
|
});
|
|
|
|
// Si on arrive ici, le téléchargement a réussi
|
|
return;
|
|
|
|
} catch (error) {
|
|
lastError = error;
|
|
console.error(`Download attempt ${attempt + 1} failed:`, error.code || error.message);
|
|
|
|
// Nettoyer le fichier partiel en cas d'erreur
|
|
if (fs.existsSync(dest)) {
|
|
try {
|
|
fs.unlinkSync(dest);
|
|
} catch (cleanupError) {
|
|
console.warn('Could not cleanup partial file:', cleanupError.message);
|
|
}
|
|
}
|
|
|
|
// Vérifier si c'est une erreur réseau que l'on peut retry
|
|
const retryableErrors = ['ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'EPROTO'];
|
|
const isRetryable = retryableErrors.includes(error.code) ||
|
|
error.message.includes('timeout') ||
|
|
error.message.includes('stalled') ||
|
|
(error.response && error.response.status >= 500);
|
|
|
|
if (!isRetryable || attempt === maxRetries - 1) {
|
|
console.error(`Non-retryable error or max retries reached: ${error.code || error.message}`);
|
|
|
|
break;
|
|
}
|
|
|
|
console.log(`Retryable error detected, will retry in ${2000 * (attempt + 1)}ms...`);
|
|
}
|
|
}
|
|
|
|
throw new Error(`Download failed after ${maxRetries} attempts. Last error: ${lastError?.code || lastError?.message || 'Unknown error'}`);
|
|
}
|
|
|
|
function findHomePageUIPath(gameLatest) {
|
|
function searchDirectory(dir) {
|
|
try {
|
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
if (item.isFile() && item.name === 'HomePage.ui') {
|
|
return path.join(dir, item.name);
|
|
} else if (item.isDirectory()) {
|
|
const found = searchDirectory(path.join(dir, item.name));
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!fs.existsSync(gameLatest)) {
|
|
return null;
|
|
}
|
|
|
|
return searchDirectory(gameLatest);
|
|
}
|
|
|
|
function findLogoPath(gameLatest) {
|
|
function searchDirectory(dir) {
|
|
try {
|
|
const items = fs.readdirSync(dir, { withFileTypes: true });
|
|
|
|
for (const item of items) {
|
|
if (item.isFile() && item.name === 'Logo@2x.png') {
|
|
return path.join(dir, item.name);
|
|
} else if (item.isDirectory()) {
|
|
const found = searchDirectory(path.join(dir, item.name));
|
|
if (found) {
|
|
return found;
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
if (!fs.existsSync(gameLatest)) {
|
|
return null;
|
|
}
|
|
|
|
return searchDirectory(gameLatest);
|
|
}
|
|
|
|
module.exports = {
|
|
downloadFile,
|
|
findHomePageUIPath,
|
|
findLogoPath
|
|
};
|