mirror of
https://gitea.shironeko-all.duckdns.org/shironeko/Hytale-F2P-2.git
synced 2026-02-26 02:31:46 -03:00
Merge branch 'develop' into develop
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -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
149
README.md
@@ -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
|
||||
|
||||
@@ -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' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
39
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();
|
||||
});
|
||||
|
||||
|
||||
13
package-lock.json
generated
13
package-lock.json
generated
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user