diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 65bbe02..04eabb7 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -83,7 +83,7 @@ jobs:
runs-on: ubuntu-latest
if: |
startsWith(github.ref, 'refs/tags/v') ||
- github.ref == 'refs/heads/release' ||
+ github.ref == 'refs/heads/main' ||
github.event_name == 'workflow_dispatch'
permissions:
diff --git a/README.md b/README.md
index 379f3c5..8ace905 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
@@ -15,7 +15,7 @@
⭐ **If you find this project useful, please give it a STAR!** ⭐
-⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
+### ⚠️ **READ [QUICK START](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-quick-start) before Downloading & Installing the Launcher!** ⚠️
🛑 **Found a problem? Join the Discord and Select #Open-A-Ticket!: https://discord.gg/gME8rUy3MB** 🛑
@@ -27,7 +27,7 @@
- View Hytale F2P Gallery
+ View Gallery
|
@@ -51,11 +51,11 @@
|
- In-Game Screenshot-1
+ In-Game Screenshot - Spawn Point
|
- In-Game Screenshot-2
+ In-Game Screenshot - Gameplay Terrain
|
@@ -89,7 +89,7 @@
### 🎮 Hytale Hardware Requirements
-> [!INFO]
+> [!IMPORTANT]
> Hytale is designed to be accessible while scaling for high-end performance.
> Below are the [official system requirements for the Early Access](https://hytale.com/news/2025/12/hytale-hardware-requirements) release.
@@ -171,27 +171,77 @@
## 📥 Installation
-### 🪟 Windows
-1. Make sure you have installed all [**Windows Prequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) above.
-2. Download the latest `Hytale-F2P-Launcher.exe` from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/).
-3. Run the EXE.
-4. Launch from Desktop or Start menu.
+### 🪟 Windows Installation
-### 🐧 Linux
-1. Make sure you have installed all [**Linux Prequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-linux-prequisites) above.
-2. Download the latest `Hytale-F2P-Launcher.AppImage` or any specific-packages in accordance with your distro from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/).
-3. Give permission to the file (`chmod +x `).
-4. Run the file by double-clicking, or via Terminal (`./`), or find it via Desktop/App Library.
+1. **Prerequisites:** Ensure you have installed all [**Windows Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-windows-prequisites) listed above.
+2. **Download:** Get the latest `Hytale-F2P-Launcher.exe` from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
+3. **SmartScreen Note:** Since the executable is currently unsigned, Windows may show a "Windows protected your PC" popup.
+ * Click **More info**.
+ * Click **Run anyway**.
+4. **Launch:** Once installed, you can launch the app directly from your Desktop or the Start menu.
-### 🍎 macOS
-1. Download .DMG file from the from [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/).
-2. Run the file.
-3. If says "Apple could not verify ...", go to System Settings > Privacy & Security > Scroll to bottom, find "Hytale F2P Launcher" > press Open Anyway.
-4. Advanced: You can also use the .zip. // TODO: NEEDS MORE INFORMATION
+---
+
+### 🐧 Linux Installation
+
+1. **Prerequisites:** Ensure you have installed all [**Linux Prerequisites**](https://github.com/amiayweb/Hytale-F2P/tree/main?tab=readme-ov-file#-linux-prequisites) above.
+2. **Download:** Choose the package that fits your distribution from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page:
+ * **Universal:** `.AppImage`
+ * **Arch Linux:** `.pkg.tar.zst`
+ * **Fedora/RHEL/openSUSE:** `.rpm`
+ * **Debian/Ubuntu:** `.deb`
+3. **Permissions & Execution:**
+ * **AppImage:** Make the file executable and run it:
+ ```bash
+ chmod +x Hytale-F2P-Launcher.AppImage
+ ./Hytale-F2P-Launcher.AppImage
+ ```
+ * **Fedora (dnf):** Install the RPM:
+ ```bash
+ sudo dnf install ./Hytale-F2P-Launcher.rpm
+ ```
+ * **Debian/Ubuntu (apt):** Install the DEB:
+ ```bash
+ sudo apt install ./Hytale-F2P-Launcher.deb
+ ```
+ * **Arch Linux (pacman):** Install the package using:
+ ```bash
+ sudo pacman -U /path/to/Hytale-F2P-Launcher.pkg.tar.zst
+ ```
+4. **Troubleshooting:**
+ * **FUSE:** If the AppImage fails to launch on newer distributions, ensure `libfuse2` (or `fuse2` on Arch/Fedora) is installed.
+ * **Desktop Entry:** After installing via `.rpm`, `.deb`, or `.pkg.tar.zst`, the launcher should automatically appear in your App Library/Grid.
+
+---
+
+### 🍎 macOS Installation
+
+> [!NOTE]
+> Apple Silicon Users: If you are on an M1, M2, or M3 Mac, you may be prompted to install Rosetta 2 the first time you run the launcher. This is normal and required for compatibility.
+
+1. **Download:** Get the latest `.dmg` file from the [**Releases**](https://github.com/amiayweb/Hytale-F2P/releases/latest/) page.
+2. **Mount:** Double-click the `.dmg` file to open it.
+3. **Install:** Drag the **Hytale F2P Launcher** icon into your **Applications** folder.
+4. **First Run:** If macOS prevents the app from opening because it is from an "unidentified developer":
+ * Open **System Settings** > **Privacy & Security**.
+ * Scroll down to the **Security** section.
+ * Look for the message regarding "Hytale F2P Launcher" and click **Open Anyway**.
+ * Authenticate with your password and click **Open**.
+
+#### **Advanced: Manual Installation (.zip)**
+The `.zip` version is useful for users who prefer a portable installation or need to bypass specific permission issues.
+
+1. **Extract:** Download and unzip the file to your desired location (e.g., `~/Applications`).
+2. **Remove Quarantine:** macOS often "quarantines" apps downloaded via browser. If the app won't open, open **Terminal** and run:
+ ```bash
+ xattr -rd com.apple.quarantine /path/to/Hytale-F2P-Launcher.app
+ ```
+> [!TIP]
+> Type the first part of the command, then drag the app icon into the Terminal window to auto-fill the path.
---
-# Server
+# How to Host a Server
## Host your Singleplayer Server (Online-Play Feature)
@@ -205,15 +255,17 @@
## Dedicated Server
> [!NOTE]
-> Only Hytale-F2P-Server.rar file is needed to set it up on non-playing hardware (such as VPS/server hosting).
-> Only HytaleServer.jar needed to use your "Server" folder made by the launcher to host local dedicated server.
-> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router is not possible.
+> If you have already `HytaleServer.jar` in `HytaleF2P/{release/pre-release}/package/game/latest/Server`, you can use it to host local dedicated server.
+
+> [!TIP]
+> Use services like Playit.gg, Tailscale, Radmin VPN to share UDP connection if setting up router as an admin is not possible.
+
+> [!WARNING]
+> `Hytale-F2P-Server.rar` file is needed to set up a server on non-playing hardware (such as VPS/server hosting).
> [!IMPORTANT]
> See detailed information of setting up a server here: [SERVER.md](SERVER.md)
-// TODO: Server.md would be used as a detailed information to avoid confuses)
-
---
## 🛠️ Building from Source
@@ -222,29 +274,21 @@ See [BUILD.md](BUILD.md) for comprehensive build instructions.
---
-// TODO: this part needs to be written in dev notes
-
-## 📌 Versioning Policy
-
-**⚠️ Important: Semantic Versioning Required**
-
-This project follows **strict semantic versioning** with **numerical versions only**:
-
-- ✅ **Valid**: `2.0.1`, `2.0.11`, `2.1.0`, `3.0.0`
-- ❌ **Invalid**: `2.0.2b`, `2.0.2a`, `2.0.1-beta`, `v2.0.2b`
-
-**Format**: `MAJOR.MINOR.PATCH` (e.g., `2.0.11`)
-
-- **MAJOR**: Breaking changes
-- **MINOR**: New features (backward compatible)
-- **PATCH**: Bug fixes (backward compatible)
-
-**Why?** The auto-update system requires semantic versioning for proper version comparison. Letter suffixes (like `2.0.2b`) are not supported and will cause update detection issues.
-
----
-
## 📋 Changelog
-// TODO: CHANGELOG SHOULD BE IN `CHANGELOG.MD`
+
+### 🆕 v2.1.0
+
+- 🚨 **Auto-Retry Downloads and Auto-Patch Files** —
+- ⚡ **Hardware Acceleration** —
+- 👨💻 **In-App Logging** —
+- 🛠️ **Repair Button** — Y
+- 🔎 **Browse CurseForge Mods** — Browsing mods now easier with our dedicated CurseForge API Key.
+- 🌎 **Fixes and Release New Translation** — Fixed 🇪🇸 🇧🇷 and added more translation for current build. Turkish 🇹🇷 language now added.
+
+
+
+
+Click here to see older Changelogs
### 🆕 v2.0.2b *(Minor Update: Performance & Utilities)*
- 🌎 **Language Translation** — A big welcome for Spanish 🇪🇸 and Portuguese (Brazil) 🇧🇷 players! **Language setting can be found in the bottom part of Settings pane.**
@@ -253,14 +297,15 @@ This project follows **strict semantic versioning** with **numerical versions on
- 🛠️ **Repair Button** — Your game's broken? One button will fix them, go to Settings pane to Repair your game in one-click, **without losing any data**. If doing so did not fix your issue, please report it to us immediately!
- 🐛 **Fixed Bugs** — Fixed issue [#84](https://github.com/amiayweb/Hytale-F2P/issues/84) where mods disappearing when game starts in previous launcher (v2.0.2a).
-### 🆕 v2.0.2a *(Minor Update)*
+
+### 🔄 v2.0.2a *(Minor Update)*
- 🧑🚀 **Profiles System** — Added proper profile management: create, switch, and delete profiles. Each profile now has its own **isolated mod list**.
- 🔒 **Mod Isolation** — Fixed ModManager so mods are **strictly scoped to the active profile**. Browsing and installing now only affects the selected profile.
- 🚨 **Critical Path Fix** — Resolved a macOS bug where mods were being saved to a Windows path (`~/AppData/Local`) instead of `~/Library/Application Support`.
- 🛡️ **Stability Improvements** — Added an **auto-sync step before every launch** to ensure the physical mods folder always matches the active profile.
- 🎨 **UI Enhancements** — Added a **profile selector dropdown** and a **profile management modal**.
-### 🆕 v2.0.2
+### 🔄 v2.0.2
- 🎮 **Discord RPC Integration** - Added Discord Rich Presence with toggle in settings (enabled by default)
- 🌐 **Cross-Platform Multiplayer** - Added multiplayer patch support for Windows, Linux, and macOS
- 🎨 **Chat Improvements** - Simplified chat color system
@@ -305,7 +350,7 @@ This project follows **strict semantic versioning** with **numerical versions on
- ☕ **Java Management** - Automatic Java runtime handling
- 🎨 **Modern Interface** - Clean, intuitive design
- 🌟 **First Release** - Core launcher functionality
-
+
---
## 👥 Contributors
diff --git a/backend/managers/gameLauncher.js b/backend/managers/gameLauncher.js
index 9ca0e18..6a7a379 100644
--- a/backend/managers/gameLauncher.js
+++ b/backend/managers/gameLauncher.js
@@ -162,7 +162,7 @@ async function launchGame(playerName = 'Player', progressCallback, javaPathOverr
console.log(`Force patching game binaries for ${authDomain}...`);
const patchResult = await clientPatcher.ensureClientPatched(gameLatest, (msg, percent) => {
- console.log(`[Patcher] ${msg}`);
+ // console.log(`[Patcher] ${msg}`);
if (progressCallback && msg) {
progressCallback(msg, percent, null, null, null);
}
@@ -331,6 +331,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
});
+ // Monitor game process status in background
setTimeout(() => {
if (!hasExited) {
console.log('Game appears to be running successfully');
@@ -343,6 +344,7 @@ exec "$REAL_JAVA" "\${ARGS[@]}"
}
}, 3000);
+ // Return immediately, don't wait for setTimeout
return { success: true, installed: true, launched: true, pid: child.pid };
} catch (spawnError) {
console.error(`Error spawning game process: ${spawnError.message}`);
@@ -404,13 +406,22 @@ async function launchGameWithVersionCheck(playerName = 'Player', progressCallbac
progressCallback('Launching game...', 80, null, null, null);
}
- return await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
+ const launchResult = await launchGame(playerName, progressCallback, javaPathOverride, installPathOverride, gpuPreference, branch);
+
+ // Ensure we always return a result
+ if (!launchResult) {
+ console.error('launchGame returned null/undefined, creating fallback response');
+ return { success: false, error: 'Game launch failed - no response from launcher' };
+ }
+
+ return launchResult;
} catch (error) {
console.error('Error in version check and launch:', error);
if (progressCallback) {
progressCallback(`Error: ${error.message}`, -1, null, null, null);
}
- throw error;
+ // Always return an error response instead of throwing
+ return { success: false, error: error.message || 'Unknown launch error' };
}
}
diff --git a/backend/utils/clientPatcher.js b/backend/utils/clientPatcher.js
index 09c08df..3446fed 100644
--- a/backend/utils/clientPatcher.js
+++ b/backend/utils/clientPatcher.js
@@ -525,17 +525,16 @@ class ClientPatcher {
}
/**
- * Patch the server JAR using DualAuthPatcher for full dual auth support
- * This uses the same patcher as the Docker server for consistency
+ * Patch the server JAR by downloading pre-patched version
* @param {string} serverPath - Path to the HytaleServer.jar
* @param {function} progressCallback - Optional callback for progress updates
- * @param {string} javaPath - Path to Java executable
+ * @param {string} javaPath - Path to Java executable (unused, kept for compatibility)
* @returns {object} Result object with success status and details
*/
async patchServer(serverPath, progressCallback, javaPath = null) {
const newDomain = this.getNewDomain();
- console.log('=== Server Patcher v3.0 (DualAuth) ===');
+ console.log('=== Server Patcher TEMP SYSTEM NEED TO BE FIXED ===');
console.log(`Target: ${serverPath}`);
console.log(`Domain: ${newDomain}`);
@@ -545,13 +544,13 @@ class ClientPatcher {
return { success: false, error };
}
- // Check if already patched with DualAuth
+ // Check if already patched
const patchFlagFile = serverPath + '.dualauth_patched';
if (fs.existsSync(patchFlagFile)) {
try {
const flagData = JSON.parse(fs.readFileSync(patchFlagFile, 'utf8'));
if (flagData.domain === newDomain) {
- console.log(`Server already patched with DualAuth for ${newDomain}, skipping`);
+ console.log(`Server already patched for ${newDomain}, skipping`);
if (progressCallback) progressCallback('Server already patched', 100);
return { success: true, alreadyPatched: true };
}
@@ -560,80 +559,99 @@ class ClientPatcher {
}
}
- if (progressCallback) progressCallback('Preparing DualAuth patcher...', 10);
-
- // Find Java executable - use bundled JRE first (same as game uses)
- const java = javaPath || this.findJava();
- if (!java) {
- const error = 'Java not found. Please install the game first (it includes Java) or install Java 25 from: https://adoptium.net/';
- console.error(error);
- return { success: false, error };
- }
- console.log(`Using Java: ${java}`);
-
- // Setup patcher directory
- const patcherDir = path.join(__dirname, '..', 'patcher');
- const patcherJava = path.join(patcherDir, 'DualAuthPatcher.java');
- const libDir = path.join(patcherDir, 'lib');
-
- // Download patcher from hytale-auth-server if not present
- if (progressCallback) progressCallback('Checking patcher...', 15);
- try {
- await this.ensurePatcherDownloaded(patcherDir);
- } catch (e) {
- const error = `Failed to download DualAuthPatcher: ${e.message}`;
- console.error(error);
- return { success: false, error };
- }
-
- if (!fs.existsSync(patcherJava)) {
- const error = `DualAuthPatcher.java not found at ${patcherJava}`;
- console.error(error);
- return { success: false, error };
- }
-
- // Download ASM libraries if not present
- if (progressCallback) progressCallback('Checking ASM libraries...', 20);
- await this.ensureAsmLibraries(libDir);
-
- // Compile patcher if needed
- if (progressCallback) progressCallback('Compiling patcher...', 30);
- const compileResult = await this.compileDualAuthPatcher(java, patcherDir, libDir);
- if (!compileResult.success) {
- return { success: false, error: compileResult.error };
- }
-
// Create backup
- if (progressCallback) progressCallback('Creating backup...', 40);
+ if (progressCallback) progressCallback('Creating backup...', 10);
console.log('Creating backup...');
this.backupClient(serverPath);
- // Run the patcher
- if (progressCallback) progressCallback('Patching server JAR...', 50);
- console.log('Running DualAuthPatcher...');
+ // Download pre-patched JAR
+ if (progressCallback) progressCallback('Downloading patched server JAR...', 30);
+ console.log('Downloading pre-patched HytaleServer.jar');
- const classpath = [
- patcherDir,
- path.join(libDir, 'asm-9.6.jar'),
- path.join(libDir, 'asm-tree-9.6.jar'),
- path.join(libDir, 'asm-util-9.6.jar')
- ].join(process.platform === 'win32' ? ';' : ':');
+ try {
+ const https = require('https');
+ const url = 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar';
- const patchResult = await this.runDualAuthPatcher(java, classpath, serverPath, newDomain);
+ await new Promise((resolve, reject) => {
+ https.get(url, (response) => {
+ if (response.statusCode === 302 || response.statusCode === 301) {
+ // Follow redirect
+ https.get(response.headers.location, (redirectResponse) => {
+ if (redirectResponse.statusCode !== 200) {
+ reject(new Error(`Failed to download: HTTP ${redirectResponse.statusCode}`));
+ return;
+ }
+
+ const file = fs.createWriteStream(serverPath);
+ const totalSize = parseInt(redirectResponse.headers['content-length'], 10);
+ let downloaded = 0;
+
+ redirectResponse.on('data', (chunk) => {
+ downloaded += chunk.length;
+ if (progressCallback && totalSize) {
+ const percent = 30 + Math.floor((downloaded / totalSize) * 60);
+ progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
+ }
+ });
+
+ redirectResponse.pipe(file);
+ file.on('finish', () => {
+ file.close();
+ resolve();
+ });
+ }).on('error', reject);
+ } else if (response.statusCode === 200) {
+ const file = fs.createWriteStream(serverPath);
+ const totalSize = parseInt(response.headers['content-length'], 10);
+ let downloaded = 0;
+
+ response.on('data', (chunk) => {
+ downloaded += chunk.length;
+ if (progressCallback && totalSize) {
+ const percent = 30 + Math.floor((downloaded / totalSize) * 60);
+ progressCallback(`Downloading... ${(downloaded / 1024 / 1024).toFixed(2)} MB`, percent);
+ }
+ });
+
+ response.pipe(file);
+ file.on('finish', () => {
+ file.close();
+ resolve();
+ });
+ } else {
+ reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
+ }
+ }).on('error', (err) => {
+ fs.unlink(serverPath, () => {});
+ reject(err);
+ });
+ });
+
+ console.log(' Download successful');
- if (patchResult.success) {
// Mark as patched
fs.writeFileSync(patchFlagFile, JSON.stringify({
domain: newDomain,
patchedAt: new Date().toISOString(),
- patcher: 'DualAuthPatcher'
+ patcher: 'PrePatchedDownload',
+ source: 'https://pub-027b315ece074e2e891002ca38384792.r2.dev/HytaleServer.jar'
}));
if (progressCallback) progressCallback('Server patching complete', 100);
console.log('=== Server Patching Complete ===');
- return { success: true, patchCount: patchResult.patchCount || 1 };
- } else {
- return { success: false, error: patchResult.error };
+ return { success: true, patchCount: 1 };
+
+ } catch (downloadError) {
+ console.error(`Failed to download patched JAR: ${downloadError.message}`);
+
+ // Restore backup on failure
+ const backupPath = serverPath + '.original';
+ if (fs.existsSync(backupPath)) {
+ fs.copyFileSync(backupPath, serverPath);
+ console.log('Restored backup after download failure');
+ }
+
+ return { success: false, error: `Failed to download patched server: ${downloadError.message}` };
}
}
@@ -802,10 +820,24 @@ class ClientPatcher {
].join(process.platform === 'win32' ? ';' : ':');
try {
- execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, {
+ // Fix PATH for packaged Electron apps on Windows
+ const execOptions = {
stdio: 'pipe',
- cwd: patcherDir
- });
+ cwd: patcherDir,
+ env: { ...process.env }
+ };
+
+ // Add system32 to PATH for Windows to find cmd.exe
+ if (process.platform === 'win32') {
+ const systemRoot = process.env.SystemRoot || 'C:\\WINDOWS';
+ const systemPath = `${systemRoot}\\system32;${systemRoot};${systemRoot}\\System32\\Wbem`;
+ execOptions.env.PATH = execOptions.env.PATH
+ ? `${systemPath};${execOptions.env.PATH}`
+ : systemPath;
+ execOptions.shell = true;
+ }
+
+ execSync(`"${javac}" -cp "${classpath}" -d "${patcherDir}" "${patcherJava}"`, execOptions);
console.log(' Compilation successful');
return { success: true };
} catch (e) {
diff --git a/backend/utils/fileManager.js b/backend/utils/fileManager.js
index bad80b3..e0c4bbd 100644
--- a/backend/utils/fileManager.js
+++ b/backend/utils/fileManager.js
@@ -58,11 +58,11 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
console.log(`Download attempt ${attempt + 1}/${maxRetries} for ${url}`);
if (attempt > 0 && progressCallback) {
- // Exponential backoff with jitter
- const baseDelay = 2000;
+ // Exponential backoff with jitter - longer delays for unstable connections
+ const baseDelay = 3000;
const exponentialDelay = baseDelay * Math.pow(2, attempt - 1);
- const jitter = Math.random() * 1000;
- const delay = Math.min(exponentialDelay + jitter, 30000);
+ const jitter = Math.random() * 2000;
+ const delay = Math.min(exponentialDelay + jitter, 60000);
progressCallback(`Retry ${attempt}/${maxRetries - 1}...`, null, null, null, null, retryState);
await new Promise(resolve => setTimeout(resolve, delay));
@@ -78,9 +78,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
const now = Date.now();
const timeSinceLastProgress = now - lastProgressTime;
- // Only timeout if no data received for 5 minutes (300 seconds)
- if (timeSinceLastProgress > 300000 && hasReceivedData) {
- console.log('Download stalled for 5 minutes, aborting...');
+ // Only timeout if no data received for 15 minutes (900 seconds) - for very slow connections
+ if (timeSinceLastProgress > 900000 && hasReceivedData) {
+ console.log('Download stalled for 15 minutes, aborting...');
console.log(`Download had progress before stall: ${(downloaded / 1024 / 1024).toFixed(2)} MB`);
controller.abort();
}
@@ -91,6 +91,12 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
if (fs.existsSync(dest)) {
const existingStats = fs.statSync(dest);
+ // If file size matches remote size, skip download
+ if (existingStats.size == fs.statSync(dest).size) {
+ console.log('File already exists and is complete. Skipping download.');
+ return { success: true, downloaded: existingStats.size };
+ }
+
// Only resume if file exists and is substantial (> 1MB)
if (existingStats.size > 1024 * 1024) {
startByte = existingStats.size;
@@ -119,7 +125,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
method: 'GET',
url: url,
responseType: 'stream',
- timeout: 60000,
+ timeout: 120000, // 120 seconds for slow connections
signal: controller.signal,
headers: headers,
validateStatus: function (status) {
@@ -135,7 +141,7 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
lastProgressTime = Date.now();
const startTime = Date.now();
- // Check network status before attempting download
+ // Check network status before attempting download, in case of known offline state
try {
const isNetworkOnline = await checkNetworkConnection();
if (!isNetworkOnline) {
@@ -403,8 +409,9 @@ async function downloadFile(url, dest, progressCallback, maxRetries = 5) {
const retryableErrors = [
'ECONNRESET', 'ENOTFOUND', 'ECONNREFUSED', 'ETIMEDOUT',
'ESOCKETTIMEDOUT', 'EPROTO', 'ENETDOWN', 'EHOSTUNREACH',
+ 'ECONNABORTED', 'EPIPE', 'ENETRESET', 'EADDRNOTAVAIL',
'ERR_NETWORK', 'ERR_INTERNET_DISCONNECTED', 'ERR_CONNECTION_RESET',
- 'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED'
+ 'ERR_CONNECTION_TIMED_OUT', 'ERR_NAME_NOT_RESOLVED', 'ERR_CONNECTION_CLOSED'
];
const isRetryable = retryableErrors.includes(error.code) ||
diff --git a/main.js b/main.js
index 08fcdfc..2d8c8f4 100644
--- a/main.js
+++ b/main.js
@@ -41,7 +41,7 @@ let mainWindow;
let discordRPC = null;
// Discord Rich Presence setup
-const DISCORD_CLIENT_ID = 1462244937868513373;
+const DISCORD_CLIENT_ID = "1462244937868513373";
function initDiscordRPC() {
try {
@@ -93,7 +93,7 @@ function setDiscordActivity() {
}
}
-function toggleDiscordRPC(enabled) {
+async function toggleDiscordRPC(enabled) {
console.log('Toggling Discord RPC:', enabled);
if (enabled && !discordRPC) {
@@ -103,11 +103,12 @@ function toggleDiscordRPC(enabled) {
try {
console.log('Disconnecting Discord RPC...');
discordRPC.clearActivity();
+ await new Promise(r => setTimeout(r, 100));
discordRPC.destroy();
- discordRPC = null;
console.log('Discord RPC disconnected successfully');
} catch (error) {
console.error('Error disconnecting Discord RPC:', error.message);
+ } finally {
discordRPC = null;
}
}
@@ -378,23 +379,18 @@ app.whenReady().then(async () => {
}, 3000);
});
-function cleanupDiscordRPC() {
- if (discordRPC) {
- try {
- console.log('Cleaning up Discord RPC...');
- discordRPC.clearActivity();
- setTimeout(() => {
- try {
- discordRPC.destroy();
- } catch (error) {
- console.log('Error during final Discord RPC cleanup:', error.message);
- }
- }, 100);
- discordRPC = null;
- } catch (error) {
- console.log('Error cleaning up Discord RPC:', error.message);
- discordRPC = null;
- }
+async function cleanupDiscordRPC() {
+ if (!discordRPC) return;
+ try {
+ console.log('Cleaning up Discord RPC...');
+ discordRPC.clearActivity();
+ await new Promise(r => setTimeout(r, 100));
+ discordRPC.destroy();
+ console.log('Discord RPC cleaned up successfully');
+ } catch (error) {
+ console.log('Error cleaning up Discord RPC:', error.message);
+ } finally {
+ discordRPC = null;
}
}
@@ -405,9 +401,6 @@ app.on('before-quit', () => {
app.on('window-all-closed', () => {
console.log('=== LAUNCHER CLOSING ===');
-
- cleanupDiscordRPC();
-
app.quit();
});
diff --git a/package-lock.json b/package-lock.json
index 2da2ede..12d6ac3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"adm-zip": "^0.5.10",
"axios": "^1.6.0",
"discord-rpc": "^4.0.1",
+ "dotenv": "^17.2.3",
"electron-updater": "^6.7.3",
"fs-extra": "^11.3.3",
"tar": "^6.2.1",
@@ -1906,6 +1907,18 @@
"node": ">=8"
}
},
+ "node_modules/dotenv": {
+ "version": "17.2.3",
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://dotenvx.com"
+ }
+ },
"node_modules/dotenv-expand": {
"version": "11.0.7",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz",