From b05aeef66d126c8da88b9a1c5d7651d8bb64cfb9 Mon Sep 17 00:00:00 2001 From: Rahul Sahani <110347707+Rahul-Sahani04@users.noreply.github.com> Date: Wed, 21 Jan 2026 03:57:33 +0530 Subject: [PATCH] feat: Add Repair Game button, UserData backup and cache clearing (#79) * 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 --- GUI/index.html | 190 ++++++++++++++---------------- GUI/js/launcher.js | 105 +++++++++++++++-- GUI/js/logs.js | 96 +++++++++++++++ GUI/js/script.js | 7 +- GUI/style.css | 90 ++++++++++++++ backend/launcher.js | 26 +++-- backend/managers/gameManager.js | 200 ++++++++++++++++++++++++-------- backend/utils/fileManager.js | 49 ++++---- main.js | 54 ++++++++- preload.js | 2 + 10 files changed, 616 insertions(+), 203 deletions(-) create mode 100644 GUI/js/logs.js diff --git a/GUI/index.html b/GUI/index.html index b97cf8f..f2d54b4 100644 --- a/GUI/index.html +++ b/GUI/index.html @@ -55,6 +55,10 @@ Skins + @@ -279,61 +283,6 @@
-
-

- - Java Runtime -

- -
- -
- - -
- -
-

- - Discord Integration -

- -
- -
-

@@ -366,39 +315,39 @@

-
- -
- - - - - - -
-

- - Select your preferred GPU (Linux: affects DRI_PRIME) -

-
-
- - -
-

- - Discord Integration -

-
-
@@ -435,41 +384,57 @@
Manage All UUIDs
-
View and manage all player UUIDs
+
View and manage all player UUIDs +
+
+

+ + Discord Integration +

+ +
+ +
+
+

Java Runtime

- +
- + - +
@@ -494,8 +459,32 @@

Skin customization coming soon...

+ +
+
+
+

+ + SYSTEM LOGS +

+
+ + + +
+
+
+
Loading logs...
+
+
+
- @@ -742,4 +731,5 @@ - \ No newline at end of file + + diff --git a/GUI/js/launcher.js b/GUI/js/launcher.js index 2aa78ac..047078d 100644 --- a/GUI/js/launcher.js +++ b/GUI/js/launcher.js @@ -23,14 +23,6 @@ export function setupLauncher() { javaPathInput.addEventListener('change', saveJavaPath); } - if (window.electronAPI && window.electronAPI.onProgressUpdate) { - window.electronAPI.onProgressUpdate((data) => { - if (!isDownloading) return; - if (window.LauncherUI) { - window.LauncherUI.updateProgress(data); - } - }); - } if (window.electronAPI && window.electronAPI.onProgressUpdate) { window.electronAPI.onProgressUpdate((data) => { if (!isDownloading) return; @@ -445,6 +437,49 @@ async function performUninstall() { } } +export async function repairGame() { + showCustomConfirm( + 'Are you sure you want to repair Hytale? This will reinstall the game files but keep your data (saves, screenshots, etc.).', + 'Repair Game', + async () => { + await performRepair(); + }, + null, + 'Repair', + 'Cancel' + ); +} + +async function performRepair() { + if (window.LauncherUI) window.LauncherUI.showProgress(); + if (window.LauncherUI) window.LauncherUI.updateProgress({ message: 'Repairing game...' }); + isDownloading = true; + + try { + if (window.electronAPI && window.electronAPI.repairGame) { + const result = await window.electronAPI.repairGame(); + + if (result.success) { + if (window.LauncherUI) { + window.LauncherUI.updateProgress({ message: 'Game repaired successfully!' }); + setTimeout(() => { + window.LauncherUI.hideProgress(); + }, 2000); + } + } else { + throw new Error(result.error || 'Repair failed'); + } + } + } catch (error) { + if (window.LauncherUI) { + window.LauncherUI.updateProgress({ message: `Repair failed: ${error.message}` }); + setTimeout(() => window.LauncherUI.hideProgress(), 3000); + } + } finally { + isDownloading = false; + } +} + function resetPlayButton() { isDownloading = false; if (playBtn) { @@ -537,5 +572,59 @@ async function loadCustomJavaPath() { window.launch = launch; window.uninstallGame = uninstallGame; +window.repairGame = repairGame; + +window.openLogs = async () => { + if (window.LauncherUI) { + window.LauncherUI.showPage('logs-page'); + window.LauncherUI.setActiveNav('logs'); + } + await refreshLogs(); +}; + +window.openLogsFolder = async () => { + try { + if (window.electronAPI && window.electronAPI.openLogsFolder) { + await window.electronAPI.openLogsFolder(); + } + } catch (error) { + console.error('Failed to open logs folder:', error); + } +}; + +window.refreshLogs = async () => { + const terminal = document.getElementById('logsTerminal'); + if (!terminal) return; + + try { + if (window.electronAPI && window.electronAPI.getRecentLogs) { + // Fetch up to MAX_LOG_LINES lines + const logs = await window.electronAPI.getRecentLogs(MAX_LOG_LINES); + if (logs) { + // Formatting for colors could be done here if needed + terminal.textContent = logs; + terminal.scrollTop = terminal.scrollHeight; + } else { + terminal.textContent = 'No logs available.'; + } + } + } catch (error) { + terminal.textContent = 'Error loading logs: ' + error.message; + } +}; + +window.copyLogs = () => { + const terminal = document.getElementById('logsTerminal'); + if (terminal) { + navigator.clipboard.writeText(terminal.textContent) + .then(() => alert('Logs copied to clipboard!')) + .catch(err => console.error('Failed to copy logs:', err)); + } +}; + +window.repairGame = repairGame; + +// Constants +const MAX_LOG_LINES = 500; document.addEventListener('DOMContentLoaded', setupLauncher); diff --git a/GUI/js/logs.js b/GUI/js/logs.js new file mode 100644 index 0000000..ecc21a0 --- /dev/null +++ b/GUI/js/logs.js @@ -0,0 +1,96 @@ + +// Logs Page Logic + +async function loadLogs() { + const terminal = document.getElementById('logsTerminal'); + if (!terminal) return; + + terminal.innerHTML = '
Loading logs...
'; + + try { + const logs = await window.electronAPI.getRecentLogs(500); // Fetch last 500 lines + + if (logs) { + // Escape HTML to prevent XSS and preserve format + const safeLogs = logs.replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + + terminal.innerHTML = `
${safeLogs}
`; + + // Auto scroll to bottom + terminal.scrollTop = terminal.scrollHeight; + } else { + terminal.innerHTML = '
No logs found.
'; + } + } catch (error) { + console.error('Failed to load logs:', error); + terminal.innerHTML = `
Error loading logs: ${error.message}
`; + } +} + +async function refreshLogs() { + const btn = document.querySelector('button[onclick="refreshLogs()"] i'); + if (btn) btn.classList.add('fa-spin'); + + await loadLogs(); + + if (btn) setTimeout(() => btn.classList.remove('fa-spin'), 500); +} + +async function copyLogs() { + const terminal = document.getElementById('logsTerminal'); + if (!terminal) return; + + const content = terminal.innerText; + if (!content) return; + + try { + await navigator.clipboard.writeText(content); + + const btn = document.querySelector('button[onclick="copyLogs()"]'); + const originalText = btn.innerHTML; + + btn.innerHTML = ' Copied!'; + setTimeout(() => { + btn.innerHTML = originalText; + }, 2000); + } catch (err) { + console.error('Failed to copy logs:', err); + } +} + +async function openLogsFolder() { + await window.electronAPI.openLogsFolder(); +} + +function openLogs() { + // Navigation is handled by sidebar logic, but we can trigger a refresh + window.LauncherUI.showPage('logs-page'); + window.LauncherUI.setActiveNav('logs'); + refreshLogs(); +} + +// Expose functions globally +window.refreshLogs = refreshLogs; +window.copyLogs = copyLogs; +window.openLogsFolder = openLogsFolder; +window.openLogs = openLogs; + +// Auto-load logs when the page becomes active +const logsObserver = new MutationObserver((mutations) => { + mutations.forEach((mutation) => { + if (mutation.target.classList.contains('active') && mutation.target.id === 'logs-page') { + loadLogs(); + } + }); +}); + +document.addEventListener('DOMContentLoaded', () => { + const logsPage = document.getElementById('logs-page'); + if (logsPage) { + logsObserver.observe(logsPage, { attributes: true, attributeFilter: ['class'] }); + } +}); diff --git a/GUI/js/script.js b/GUI/js/script.js index 0d90bef..7741528 100644 --- a/GUI/js/script.js +++ b/GUI/js/script.js @@ -6,8 +6,9 @@ import './mods.js'; import './players.js'; import './chat.js'; import './settings.js'; +import './logs.js'; -window.closeDiscordNotification = function() { +window.closeDiscordNotification = function () { const notification = document.getElementById('discordNotification'); if (notification) { notification.classList.add('hidden'); @@ -24,7 +25,7 @@ document.addEventListener('DOMContentLoaded', () => { if (!dismissed) { setTimeout(() => { notification.style.display = 'flex'; - }, 3000); + }, 3000); } else { notification.style.display = 'none'; } @@ -32,7 +33,7 @@ document.addEventListener('DOMContentLoaded', () => { }); const originalClose = window.closeDiscordNotification; -window.closeDiscordNotification = function() { +window.closeDiscordNotification = function () { localStorage.setItem('discordNotificationDismissed', 'true'); originalClose(); }; \ No newline at end of file diff --git a/GUI/style.css b/GUI/style.css index ae95b6b..b14342f 100644 --- a/GUI/style.css +++ b/GUI/style.css @@ -795,6 +795,96 @@ body { } +.custom-java-hint i { + color: #3b82f6; +} + +/* Logs Page Styles */ +.logs-container { + display: flex; + flex-direction: column; + height: 100%; + padding: 1rem 2rem 2rem; +} + +.logs-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.logs-title { + font-size: 2rem; + font-weight: 700; + color: white; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.logs-actions { + display: flex; + gap: 0.75rem; +} + +.logs-action-btn { + padding: 0.5rem 1rem; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: #d1d5db; + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.logs-action-btn:hover { + background: rgba(255, 255, 255, 0.2); + color: white; + border-color: rgba(255, 255, 255, 0.2); +} + +.logs-terminal { + flex: 1; + background: #0d1117; + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 8px; + padding: 1rem; + font-family: 'JetBrains Mono', monospace; + font-size: 0.875rem; + line-height: 1.5; + color: #e5e7eb; + overflow-y: auto; + white-space: pre-wrap; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.5); + + /* Custom scrollbar */ + scrollbar-width: thin; + scrollbar-color: rgba(147, 51, 234, 0.3) transparent; +} + +.log-line { + margin-bottom: 0.25rem; +} + +.log-line.error { + color: #ef4444; +} + +.log-line.warn { + color: #f59e0b; +} + +.log-line.info { + color: #3b82f6; +} + + .news-section { padding: 1rem 2rem; flex: 1; diff --git a/backend/launcher.js b/backend/launcher.js index 0299639..c6c2f38 100644 --- a/backend/launcher.js +++ b/backend/launcher.js @@ -40,7 +40,8 @@ const { installGame, uninstallGame, updateGameFiles, - checkExistingGameInstallation + checkExistingGameInstallation, + repairGame } = require('./managers/gameManager'); const { @@ -87,13 +88,14 @@ module.exports = { // Game launch functions launchGame, launchGameWithVersionCheck, - + // Game installation functions installGame, isGameInstalled, uninstallGame, updateGameFiles, - + repairGame, + // User configuration functions saveUsername, loadUsername, @@ -102,16 +104,16 @@ module.exports = { saveChatColor, loadChatColor, getUuidForUser, - + // Java configuration functions saveJavaPath, loadJavaPath, getJavaDetection, - + // Installation path functions saveInstallPath, loadInstallPath, - + // Discord RPC functions saveDiscordRPC, loadDiscordRPC, @@ -124,13 +126,13 @@ module.exports = { // Version functions getInstalledClientVersion, getLatestClientVersion, - + // News functions getHytaleNews, - + // Player ID functions getOrCreatePlayerId, - + // UUID Management functions getCurrentUuid, getAllUuidMappings, @@ -138,7 +140,7 @@ module.exports = { generateNewUuid, deleteUuidForUser, resetCurrentUserUuid, - + // Mod management functions getModsPath, loadInstalledMods, @@ -147,13 +149,13 @@ module.exports = { toggleMod, saveModsToConfig, loadModsFromConfig, - + // UI file management functions downloadAndReplaceHomePageUI, findHomePageUIPath, downloadAndReplaceLogo, findLogoPath, - + // First launch functions isFirstLaunch, markAsLaunched, diff --git a/backend/managers/gameManager.js b/backend/managers/gameManager.js index adc67ef..2d5e4e0 100644 --- a/backend/managers/gameManager.js +++ b/backend/managers/gameManager.js @@ -14,7 +14,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall const osName = getOS(); const arch = getArch(); const url = `https://game-patches.hytale.com/patches/${osName}/${arch}/${version}/0/${fileName}`; - + const dest = path.join(cacheDir, fileName); if (fs.existsSync(dest)) { @@ -25,7 +25,7 @@ async function downloadPWR(version = 'release', fileName = '4.pwr', progressCall console.log('Fetching PWR patch file:', url); await downloadFile(url, dest, progressCallback); console.log('PWR saved to:', dest); - + return dest; } @@ -33,9 +33,9 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir 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; @@ -53,11 +53,11 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir } 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}`); } @@ -69,7 +69,7 @@ async function applyPWR(pwrFile, progressCallback, gameDir = GAME_DIR, toolsDir pwrFile, gameLatest ]; - + try { await new Promise((resolve, reject) => { const child = execFile(butlerPath, args, { @@ -108,7 +108,7 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, console.log(`Updating game files to version: ${newVersion}`); tempUpdateDir = path.join(gameDir, '..', 'temp_update'); - + if (fs.existsSync(tempUpdateDir)) { fs.rmSync(tempUpdateDir, { recursive: true, force: true }); } @@ -117,26 +117,26 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, if (progressCallback) { progressCallback('Downloading new game version...', 10, null, null, null); } - + const pwrFile = await downloadPWR('release', newVersion, progressCallback, cacheDir); - + if (progressCallback) { progressCallback('Extracting new files...', 50, null, null, null); } - + await applyPWR(pwrFile, progressCallback, tempUpdateDir, toolsDir); - + if (progressCallback) { progressCallback('Replacing game files...', 80, null, null, null); } - + let userDataBackup = null; const userDataPath = findUserDataRecursive(gameDir); - + if (userDataPath && fs.existsSync(userDataPath)) { userDataBackup = path.join(gameDir, '..', 'UserData_backup_' + Date.now()); console.log(`Backing up UserData from ${userDataPath} to: ${userDataBackup}`); - + function copyRecursive(src, dest) { const stat = fs.statSync(src); if (stat.isDirectory()) { @@ -151,35 +151,35 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, fs.copyFileSync(src, dest); } } - + copyRecursive(userDataPath, userDataBackup); } else { console.log('No UserData folder found in game directory'); } - + 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); - + if (userDataBackup && fs.existsSync(userDataBackup)) { const newUserDataPath = findUserDataPath(gameDir); const userDataParent = path.dirname(newUserDataPath); - + if (!fs.existsSync(userDataParent)) { fs.mkdirSync(userDataParent, { recursive: true }); } - + console.log(`Restoring UserData to: ${newUserDataPath}`); - + function copyRecursive(src, dest) { const stat = fs.statSync(src); if (stat.isDirectory()) { @@ -194,12 +194,12 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, fs.copyFileSync(src, dest); } } - + copyRecursive(userDataBackup, newUserDataPath); } - + console.log(`Game files updated successfully to version: ${newVersion}`); - + if (userDataBackup && fs.existsSync(userDataBackup)) { try { fs.rmSync(userDataBackup, { recursive: true, force: true }); @@ -208,18 +208,18 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, console.warn('Could not clean up UserData backup:', cleanupError.message); } } - + console.log('Waiting for file system sync...'); - await new Promise(resolve => setTimeout(resolve, 2000)); - + 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 (userDataBackup && fs.existsSync(userDataBackup)) { try { fs.rmSync(userDataBackup, { recursive: true, force: true }); @@ -228,11 +228,11 @@ async function updateGameFiles(newVersion, progressCallback, gameDir = GAME_DIR, 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}`); } } @@ -309,31 +309,31 @@ async function installGame(playerName = 'Player', progressCallback, javaPathOver progressCallback('Fetching game files...', null, null, null, null); } console.log('Installing game files...'); - + const latestVersion = await getLatestClientVersion(); const pwrFile = await downloadPWR('release', latestVersion, progressCallback, customCacheDir); await applyPWR(pwrFile, progressCallback, customGameDir, customToolsDir); - + 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); - + if (progressCallback) { progressCallback('Installation complete', 100, null, null, null); } console.log('Game installation completed successfully'); - - return { - success: true, + + return { + success: true, installed: true }; } async function uninstallGame() { const appDir = getResolvedAppDir(); - + if (!fs.existsSync(appDir)) { throw new Error('Game is not installed'); } @@ -341,7 +341,7 @@ async function uninstallGame() { 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; @@ -355,25 +355,25 @@ async function uninstallGame() { function checkExistingGameInstallation() { try { const config = loadConfig(); - + if (!config.installPath || !config.installPath.trim()) { return null; } - + const installPath = config.installPath.trim(); const gameDir = path.join(installPath, 'HytaleF2P', 'release', '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, @@ -387,6 +387,102 @@ function checkExistingGameInstallation() { } } +async function repairGame(progressCallback) { + const appDir = getResolvedAppDir(); + const gameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); + + // Check if game exists + if (!fs.existsSync(gameDir)) { + throw new Error('Game directory not found. Cannot repair.'); + } + + // Locate UserData + const userDataPath = findUserDataRecursive(gameDir); + let userDataBackup = null; + + if (progressCallback) { + progressCallback('Backing up user data...', 10, null, null, null); + } + + // Backup UserData + if (userDataPath && fs.existsSync(userDataPath)) { + userDataBackup = path.join(appDir, 'UserData_backup_repair_' + Date.now()); + console.log(`Backing up UserData during repair from ${userDataPath} to ${userDataBackup}`); + + // Copy function + function copyRecursive(src, dest) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child))); + } else { + fs.copyFileSync(src, dest); + } + } + + copyRecursive(userDataPath, userDataBackup); + } + + 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); + + // Restore UserData + if (userDataBackup && fs.existsSync(userDataBackup)) { + if (progressCallback) { + progressCallback('Restoring user data...', 90, null, null, null); + } + + // installGame creates: path.join(customGameDir, 'Client', 'UserData') + const newGameDir = path.join(appDir, 'release', 'package', 'game', 'latest'); + const newUserDataPath = path.join(newGameDir, 'Client', 'UserData'); + + if (!fs.existsSync(newUserDataPath)) { + fs.mkdirSync(newUserDataPath, { recursive: true }); + } + + console.log(`Restoring UserData to ${newUserDataPath}`); + + function copyRecursive(src, dest) { + const stat = fs.statSync(src); + if (stat.isDirectory()) { + if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true }); + fs.readdirSync(src).forEach(child => copyRecursive(path.join(src, child), path.join(dest, child))); + } else { + fs.copyFileSync(src, dest); + } + } + + copyRecursive(userDataBackup, newUserDataPath); + + // Cleanup Backup + console.log('Cleaning up repair backup...'); + fs.rmSync(userDataBackup, { recursive: true, force: true }); + } + + if (progressCallback) { + progressCallback('Repair completed successfully!', 100, null, null, null); + } + + return { success: true, repaired: true }; +} + module.exports = { downloadPWR, applyPWR, @@ -394,5 +490,9 @@ module.exports = { isGameInstalled, installGame, uninstallGame, - checkExistingGameInstallation + isGameInstalled, + installGame, + uninstallGame, + checkExistingGameInstallation, + repairGame }; diff --git a/backend/utils/fileManager.js b/backend/utils/fileManager.js index 0671068..6eb5455 100644 --- a/backend/utils/fileManager.js +++ b/backend/utils/fileManager.js @@ -4,11 +4,11 @@ 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 @@ -53,19 +53,19 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) { 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; @@ -97,14 +97,14 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) { reject(new Error('Download stalled')); } }); - + writer.on('error', (error) => { if (stalledTimeout) { clearTimeout(stalledTimeout); } reject(error); }); - + response.data.on('error', (error) => { if (stalledTimeout) { clearTimeout(stalledTimeout); @@ -119,7 +119,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) { } 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 { @@ -128,23 +128,24 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 3) { 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); - + 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'}`); } @@ -152,7 +153,7 @@ 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); @@ -165,14 +166,14 @@ function findHomePageUIPath(gameLatest) { } } catch (error) { } - + return null; } - + if (!fs.existsSync(gameLatest)) { return null; } - + return searchDirectory(gameLatest); } @@ -180,7 +181,7 @@ 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); @@ -193,14 +194,14 @@ function findLogoPath(gameLatest) { } } catch (error) { } - + return null; } - + if (!fs.existsSync(gameLatest)) { return null; } - + return searchDirectory(gameLatest); } diff --git a/main.js b/main.js index 919224d..b529f1d 100644 --- a/main.js +++ b/main.js @@ -1,7 +1,7 @@ const { app, BrowserWindow, ipcMain, dialog, shell } = require('electron'); const path = require('path'); const fs = require('fs'); -const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); +const { launchGame, launchGameWithVersionCheck, installGame, saveUsername, loadUsername, saveChatUsername, loadChatUsername, saveChatColor, loadChatColor, saveJavaPath, loadJavaPath, saveInstallPath, loadInstallPath, saveDiscordRPC, loadDiscordRPC, isGameInstalled, uninstallGame, repairGame, getHytaleNews, handleFirstLaunchCheck, proposeGameUpdate, markAsLaunched } = require('./backend/launcher'); const UpdateManager = require('./backend/updateManager'); const logger = require('./backend/logger'); const profileManager = require('./backend/managers/profileManager'); @@ -120,8 +120,6 @@ function createWindow() { mainWindow.webContents.send('show-update-popup', updateInfo); } }, 3000); - //mainWindow.webContents.openDevTools(); - mainWindow.webContents.on('devtools-opened', () => { mainWindow.webContents.closeDevTools(); @@ -311,7 +309,7 @@ ipcMain.handle('launch-game', async (event, playerName, javaPath, installPath, g }; const result = await launchGameWithVersionCheck(playerName, progressCallback, javaPath, installPath, gpuPreference); - + return result; } catch (error) { console.error('Launch error:', error); @@ -469,13 +467,35 @@ ipcMain.handle('is-game-installed', async () => { ipcMain.handle('uninstall-game', async () => { try { await uninstallGame(); - return { success: true }; } catch (error) { console.error('Uninstall error:', error); return { success: false, error: error.message }; } }); +ipcMain.handle('repair-game', async () => { + try { + const progressCallback = (message, percent, speed, downloaded, total) => { + if (mainWindow && !mainWindow.isDestroyed()) { + const data = { + message: message || null, + percent: percent !== null && percent !== undefined ? Math.min(100, Math.max(0, percent)) : null, + speed: speed !== null && speed !== undefined ? speed : null, + downloaded: downloaded !== null && downloaded !== undefined ? downloaded : null, + total: total !== null && total !== undefined ? total : null + }; + mainWindow.webContents.send('progress-update', data); + } + }; + + const result = await repairGame(progressCallback); + return result; + } catch (error) { + console.error('Repair error:', error); + return { success: false, error: error.message }; + } +}); + ipcMain.handle('get-hytale-news', async () => { try { const news = await getHytaleNews(); @@ -850,7 +870,14 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { const content = fs.readFileSync(latestLogFile, 'utf8'); const lines = content.split('\n'); - return lines.slice(-maxLines).join('\n'); + let result = lines.slice(-maxLines).join('\n'); + + if (lines.length > maxLines) { + const truncatedMsg = `\n--- ⚠️ LOG TRUNCATED: Showing last ${maxLines} lines of ${lines.length}. Open Logs Folder for full history ---\n\n`; + return result + truncatedMsg; + } + + return result; } catch (error) { console.error('Error reading logs:', error); return null; @@ -858,6 +885,21 @@ ipcMain.handle('get-recent-logs', async (event, maxLines = 100) => { }); + +ipcMain.handle('open-logs-folder', async () => { + try { + const logDir = logger.getLogDirectory(); + if (logDir && fs.existsSync(logDir)) { + await shell.openPath(logDir); + return { success: true }; + } + return { success: false, error: 'Logs directory not found' }; + } catch (error) { + console.error('Error opening logs folder:', error); + return { success: false, error: error.message }; + } +}); + // Profile Management IPC ipcMain.handle('profile-create', async (event, name) => { try { diff --git a/preload.js b/preload.js index fb3aafc..03c6947 100644 --- a/preload.js +++ b/preload.js @@ -21,6 +21,7 @@ contextBridge.exposeInMainWorld('electronAPI', { browseJavaPath: () => ipcRenderer.invoke('browse-java-path'), isGameInstalled: () => ipcRenderer.invoke('is-game-installed'), uninstallGame: () => ipcRenderer.invoke('uninstall-game'), + repairGame: () => ipcRenderer.invoke('repair-game'), getHytaleNews: () => ipcRenderer.invoke('get-hytale-news'), openExternal: (url) => ipcRenderer.invoke('open-external', url), openExternalLink: (url) => ipcRenderer.invoke('openExternalLink', url), @@ -70,6 +71,7 @@ contextBridge.exposeInMainWorld('electronAPI', { }, getLogDirectory: () => ipcRenderer.invoke('get-log-directory'), + openLogsFolder: () => ipcRenderer.invoke('open-logs-folder'), getRecentLogs: (maxLines) => ipcRenderer.invoke('get-recent-logs', maxLines), // UUID Management methods