Merge branch 'develop' into develop

This commit is contained in:
Fazri Gading
2026-01-26 05:09:36 +08:00
committed by GitHub
7 changed files with 258 additions and 157 deletions

View File

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

149
README.md
View File

@@ -1,7 +1,7 @@
<div align="center">
<header>
<h1>🎮 Hytale F2P Launcher | Cross-Platform Multiplayer Support 🖥</h1>
<h1>🎮 Hytale F2P Launcher | Cross-Platform Multiplayer 🖥</h1>
<h2>Available for Windows 🪟, macOS 🍎, and Linux 🐧</h2>
<p><small>An unofficial cross-platform launcher for Hytale with automatic updates and multiplayer support (all OS supported)</small></p>
</header>
@@ -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 @@
<div align="center">
<img src="https://i.imgur.com/xW9do3d.png" alt="Hytale F2P Launcher" width="1000">
<details>
<summary><b>View Hytale F2P Gallery</b></summary>
<summary><b>View Gallery</b></summary>
<table style="width: 100%; border-spacing: 15px; border-collapse: separate;">
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
@@ -51,11 +51,11 @@
</tr>
<tr>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot-1</b><br>
<b>In-Game Screenshot - Spawn Point</b><br>
<img src="https://i.imgur.com/X8lNFQ7.png" alt="Hytale F2P In-Game Screenshot-1" width="100%">
</td>
<td align="center" style="vertical-align: top; width: 50%;">
<b>In-Game Screenshot-2</b><br>
<b>In-Game Screenshot - Gameplay Terrain</b><br>
<img src="https://i.imgur.com/3iRScPa.png" alt="Hytale F2P In-Game Screenshot-2" width="100%">
</td>
</tr>
@@ -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 <filename>`).
4. Run the file by double-clicking, or via Terminal (`./<filename>`), 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.
<details>
<summary>Click here to see older Changelogs</summary>
### 🆕 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
</details>
---
## 👥 Contributors

View File

@@ -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' };
}
}

View File

@@ -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) {

View File

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

39
main.js
View File

@@ -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();
});

13
package-lock.json generated
View File

@@ -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",